02.C++ 基础精讲

本节分为 5 大类:

  1. 形参带默认值的函数
  2. 内联函数inline
  3. 详解函数重载
  4. const 深入应用
  5. 深入理解 C++ 的 new 和 delete

1. 形参带默认值的函数

在 C++ 中,声明一个函数时,可以为函数的参数指定默认值。当调用有默认参数值的函数时,可以不写出参数,这时就相当于以默认值作为参数调用该函数。

注意事项:

  1. 在有函数声明(原型)时,默认参数可以放在函数声明或定义中,但是只能放在二者之一。
1
2
3
4
5
6
double sqrt(double f = 1.0); //函数声明

double sqrt(double f) //函数定义
{
// ....
}
  1. 没有函数(原型)时,默认参数在函数定义时指定。
1
2
3
//没有 函数声明

double sqrt(double f = 1.0) //函数定义
  1. 在具有多个参数的函数中指定默认值时,默认参数都必须出现在不默认参数的右边,一旦某个参数开始指定默认值,它右边的所有参数都必须指定默认值.

就是说,函数声明时,必须按照从右向左的顺序,依次给与默认值。

原因:

函数形参的压栈过程是从右向左。详细请看:[[01.理解 C++ 内核]] 的 从指令角度掌握函数调用堆栈详细过程。

1
2
3
int f (int i1, int i2 = 2, int i3 = 3);     // 正确
int g (int i1, int i2 = 2, int i3); // 错误, i3未指定默认值
int h (int i1 = 1, int i2, int i3 = 3); // 错误, i2未指定默认值

普通函数和形参带默认值函数对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

int sum(int a = 10, int b = 10)
{
int temp = 0;
temp = a + b;
return temp;
}

int main()
{
int a = 10;
int b = 20;

int ret = sum(a, b);

ret = sum(a);//b 使用默认值

ret = sum();//都使用默认值

return 0;
}

image.png

对比1,2 发现:

2 中 b 使用默认值,因此将 b 的值拷贝到寄存器后压栈,而是直接将常量0ah(10) 压栈,减少了此寄存器拷贝;

同理有3,使用默认值是:调用函数减少了 mov 指令。

2. 内联函数 inline

特征:

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);

// 声明2(不加 inline)
int functionName(int first, int second,...);

// 定义
inline int functionName(int first, int second,...) {/****/};

// 类内定义,隐式内联
class A {
int doA() { return 0; } // 隐式内联
}

// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() { return 0; } // 需要显式内联

编译器对 inline 函数的处理步骤:

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

内联函数与普通函数的区别?

  1. 内联函数;在编译过程中,就没有函数调用开销。在函数的调用点直接将函数的代码进行展开处理

[[01.理解 C++ 内核]] 中的 从指令角度掌握函数调用堆栈详细过程 知道,在调用函数的过程中:

(1)将函数实数从右向左压栈
(2)call指令:
将下一行要执行的代码地址入栈
跳转到函数入口:首先push ebp,将栈底指针入栈,然后给函数开辟栈帧函数执行结束后,栈帧回退。

在函数调用中,有大量的函数调用开销。如果封装的函数内容简单,函数调用的开销大于函数指令的执行时间,那么就可以使用内联函数(需要大量调用,且指令简单)。在调用点展开内联函数指令

  1. 内联函数不在生成相应的函数符号

  2. inline 只是建议编译器把这个函数处理成内联函数,具体会由编译器处理觉得是否展开成内联函数。

注意:

(1)如果用vs调试Debug,不会将函数展开成内联.release版本可以。

优缺点:

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

3. 详解函数重载

函数重载:一组函数,其中函数名相同,参数列表的个数或者类型不同,那么这一组函数就称作函数重载。函数重载发生在编译时期。

(1)函数重载与函数返回值无关,因为在产生符号时没有返回值
(2) 函数重载需要在同一个作用域
(3)const 或者 volatile 的时候,是如何影响形参的

C++ 支持函数重载,而 C 则不支持:

编译器产生的函数符号规则不同:

  • C++ 代码:函数符号包含了函数名和参数列表
  • C 代码:函数符号只包含了函数名。

注意事项:

** 函数重载需要在同一个作用域下。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

bool compare(int a, int b)
{
return a > b;
}

bool compare(double a, double b)
{
return a > b;
}

bool compare(const char* a, const char* b)
{
return a > b;
}

int main()
{
bool compare(double a, double b);
compare("adf", "wew");

return 0;
}

image.png

由于在局部作用域声明了新的 compare,导致无法重载外部作用域的 compare。

const int 和 int 的重载:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

void func(int a) {}
void func(const int a) {}

int main()
{
// ...
return 0;
}

image.png

原因:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <typeinfo>

int main()
{
int a = 10;
const int b = 10;

std::cout << typeid(a).name() << std::endl;
std::cout << typeid(b).name() << std::endl;
return 0;
}

image.png

const int 和 int 在编译器看来都是 int 类型 ,无法完成重载。

3.1 C++ 和 C 语言如何相互调用

由于 C++ 和 C 语言的编译器生成的函数符号不同,在 C++ 使用 c 语言需要使用exten “C”{};

1. C++ 调用 C

对于c++,由于c++的编译器对c语言兼容,因此在c++中调用c语言编写的函数,只需要在函数声明前面加上关键字extern "C",表示采用类c语言的方式解析函数符号。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// add.h

#ifdef __ADD_H__
#define __ADD_H__

extern "C" int add(int a, int b);

#endif


// add.c
int add(int a, int b)
{
return a + b;
}

// main.cpp
#include <iostream>
#include "add.h"
using namespace std;

int main()
{
cout << "1 + 1 = " << add(1, 1) << endl;
}

在例子中,main.cppc++ 代码,add.c 为 c 语言代码,当 c++ 编译器识别到extern "C" 关键字时,会去寻找 _add_ 函数的实现而不是寻找类似_int_add_int_int_ 这样带参数信息的函数实现。

2. C 调用 C++

c 语言调用 c++ 代码却并不容易,原因是 c 语言并不兼容 c++。就算 c 语言可以调用 c++,也会因为无法识别 c++ 新定义的符号而编译报错。因此,为了实现 c 语言调用 c++ 函数,必须实现以下两个步骤:

  1. 将 c++ 相关函数封装为静态库或动态库(因为调用库函数时编译器并不知道里面执行的是什么语言);
  2. 对外提供遵循类 c 语言规约的接口函数。例子如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// printNum.h
#ifdef __PRINTNUM_H__
#define __PRINTNUM_H__

extern "C" void printNum(int a);

#endif

// printNum.cpp
#include <iostream>
#include "printNum.h"
using namespace std;

void printNum(int a)
{
cout << << "num is " << a << endl;
}

// main.c
extern void printNum(int a);

printNum(5);

通过将 _cout_函数封装为类 c 语言规约的接口函数,使得 main.c 中可以成功调用 c++ 函数 _printNum_ 。值得注意的是,main.c 不可以直接引入 printNum.h,因为 c 语言不能识别 extern "C" 关键字。可以利用 c++ 预定义宏实现头文件的改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef __PRINTNUM_H__
#define __PRINTNUM_H__

#ifdef __cplusplus
extern "C" {
#endif
void printNum(int a);

#ifdef __cplusplus
}
#endif

#endif

3. 总结

  1. c 语言与 c++ 的相互调用可以通过 extern "C" 关键字实现
  2. c++ 中调用 c 代码,只须在 c++ 中为 c 代码函数声明之前加上 extern "C"
  3. c 语言调用 c++ 代码,则需要将 c++ 代码编译成静态库或动态库,然后对外提供用 extern "C" 声明的类 c 封装函数

4. const 深入应用

const 作用:

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
  3. 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。

const 的指针与引用:

  • 指针
    • 指向常量的指针(pointer to const)
    • 自身是常量的指针(常量指针,const pointer)
  • 引用
    • 指向常量的引用(reference to const)
    • 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 类
class A
{
private:
const int a; // 常对象成员,可以使用初始化列表或者类内初始化

public:
// 构造函数
A() : a(0) { };
A(int x) : a(x) { }; // 初始化列表

// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数
const A *p = &a; // 指针变量,指向常对象
const A &q = a; // 指向常对象的引用

// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char* const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char* const p4 = greeting; // 自身是常量的指针,指向字符数组常量
}

// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量

// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();

宏定义 #define 和 const 常量:

宏定义 #define const 常量
宏定义,相当于字符替换 常量声明
预处理器处理 编译器处理
无类型安全检查 有类型安全检查
不分配内存 要分配内存
存储在代码段 存储在数据段
可通过 #undef 取消 不可取消

1. C++ 和 C 的 const 区别

  • c语言中,const修饰的值,可以不用初始化,不叫常量,叫做常变量;

image.png

最终输出为:30、30、30

  • C++中: const 定义的类型必须初始化,否则报错,c 语言中可以不初始化

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main()
{
// const int b;
const int a = 10;
// a 常量,可以定义数组长度
int array[a] = {};

int* p = (int*)&a;
*p = 30;

cout << a << " " << *p << " " << *(&a) << endl;

return 0;
}

image.png

原因:const 的编译方式不同,C 语言中,const 就是当作一个变量来编译生成指令的。C++ 中,如果 const 赋值是一个立即数,所有出现 const 常量名字的地方,都被常量的初始化所替换。

1.1 Debug 调试

image.png

image.png

执行完第9行后 a 的内存中的值变成 1e 也即 30;但是本来出现 a 的地方在编译期已经被替换成 10,因此输出 a 依然是 10。

如果不是立即数,则是常变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int main()
{
int b = 1;
const int a = b;

// 报错,a是常变量
//int array[a] = {};

int* p = (int*)&a;
*p = 30;

cout << a << " " << *p << " " << *(&a); // 30 30 30

return 0;
}

2. const 与指针

const 修饰的量常出现的错误:

(1)常量不能再作为左值
(2)不能把常量的地址泄露给一个普通的指针或者普通的引用变量

image.png

2.1 const 和 一级指针

const 如果右边没有指针*,则const 是不参与类型的

C++的语言规范:就近原则 const 修饰的是离它最近的类型

  1. const int* p;离 const 最近的类型是 int,所以 const 修饰的是 *p ,所以 *p 无法修改值;可以指向任意 int 的内存,但是不能通过指针简介修改内存的值。
  2. int const* p* 不是类型,离 const 最近的类型为 int,*p 无法修改,同(1)
  3. int* const p;离 const 最近的类型为(int*),const 修饰的是 p,所以不能改变 p 指向的地址,但是可以修改 p 指向的地址的内容。
  4. const int* const p;不能修改 p 指向的地址和值。
1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
const int a = 10 ;
const int * p = &a;//p指向的地址的内容不能修改

return 0;
}

重点:

const 如果右边没有指针 *,则 const 是不参与类型的,仅表示 const 修饰的是一个常量,不能作为左值。

const 类型转化公式:

  • const int* <= int* 可以转换
  • int* <= const int* 错误

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <typeinfo>
using namespace std;

int main()
{
int* p = nullptr;
int* const p1 = nullptr;

cout << typeid(p).name() << endl;
cout << typeid(p1).name() << endl;

return 0;
}

示例2:

1
2
3
4
5
int a=10;
int *p1= &a;
const int *p2 = &a;// const int * <= int *
int *const p3 = &a;// int * <= int *
int *p4 = p3;//p3是int * 类型,因此没有问题

2.2 const 和 二级指针

image.png

  • const int** q;离 const 最近的类型为 int,修饰的是 **q
  • int* const* q;离 const 最近的类型为 int*,修饰的是 *q
  • int** const q;离 const 最近的类型为 int**,修饰的是 q,同时 const 右侧没有 * ,q 是 int** 类型。

转化公式:

  • int** <= const int** 错误
  • const int ** <= int ** 错误

const 与二级指针结合的时候,两边必须同时有 const 或没有 const 才能转换;

  • int** <= int* const* 是 const 和一级指针的结合,const 右边修饰的*  (等同于 int *  <= const int *  )错误的
  • int* const* <=int** (等同于const int * <= int )可以的

要看 const 右边的 * 决定 const 修饰的是类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <typeinfo.h>

int main()
{
int a = 10;
int * p = &a;
const int ** q = &p;//error

/*
const int * *q = &p; 相当于(*)q 即 p的地址,赋值了一个const int *
而p 是int *类型,把常量的地址泄露给普通的指针(p)
修改 const int * p = &a;
*/

return 0;
}

3. 引用

  1. 引用是必须初始化的,指针可以不初始化。
  2. 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以用多级指针。
  3. 定义一个引用变量和定义一个指针变量,其汇编指令是一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的。

引用的错误用法 int &a = 10; 由下面的反汇编可以知道,引用的汇编代码第一步是将引用对象的地址拷贝到寄存器中,10是常量;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <typeinfo.h>
int main()
{
int a = 10;
int * p = &a;
int &b = a;

std::cout << a << " " << b << " " << (*p) << std::endl;

*p = 20;
std::cout << a << " " << b << " " << (*p) << std::endl;


b = 30;
std::cout << a << " " << b << " " << (*p);
return 0;
}

输出:

image.png

 反汇编:指针和引用没有区别

image.png

lea eax,[a]:将 a 的地址拷贝到寄存器 eax 中

mov dword ptr [p],eax:将 eax 中的值拷贝到 p 中。

反汇编中指针和引用拷贝也是没有区别。

指针拷贝 - 1
指针拷贝 - 2

对指针和引用赋值,都是一样的:获取地址,然后赋值。

3.1 引用别名

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <typeinfo.h>

int main()
{
int array[5] = {};
int * p = array;
int(&q)[5] = array;//定义一个引用指向数组:引用即别名 sizeof(q) = sizeof(array)

std::cout << sizeof(array) << "\n" << sizeof(p) << "\n" << sizeof(q) << std::endl;//20 5 20

return 0;
}

image.png

关于定义一个引用类型,到底需不需要开辟内存空间,我认为是需要的,上面的汇编代码中,引用和指针的汇编是一模一样的;C++ 中只有 const 类型的数据,要求必须初始化。而引用也必须要初始化,所以引用是指针,还应该是 const 修饰的常指针。 一经声明不可改变。

站在宏观角度,引用也就是别名,所以不开辟看空间。

站在微观的角度,引用至少要保存一个指针,所以一定要开辟空间。站在底层实现的角度,站在 C++ 对于 C 实现包装的角度,引用就是指针。那么既然是指针至少要占用 4 个字节空间。

4. 左值引用

左值:有内存地址,有名字,值可以修改;

int a = 10; int &b =a;

int &c =10; //错误 20 是右值,20 = 40 是错误的,其值不能修改,没内存,没名字,是一个立即数;

上述代码是无法编译通过的,因为 10 无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:

1
const int &var = 10;

使用常引用来引用常量数字 10,因为此刻内存上产生了临时变量保存了 10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:

1
2
const int temp = 10; 
const int &var = temp;

根据上述分析,得出如下结论:

左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被 const 修饰成常量引用了。

那么 C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。

5. 右值引用

C++ 对于左值和右值没有标准定义,但是有一个被广泛认同的说法:

  • 可以取地址的,有名字的,非临时的就是左值;
  • 不能取地址的,没有名字的,临时的就是右值;

可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const 对象等都是左值。

从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及 const 对象)。

  1. int &&c = 10;专门用来引用右值类型,指令上,可以自动产生临时量,然后直接引用临时量   c = 1;

反汇编:

image.png

  1. 一个右值引用变量,本身是一个左值,只能用左值引用来引用它;不能用一个右值引用变量来引用一个左值
1
2
3
int && a = 1;
a = 10;
int &e = a;

5. 深入理解 C++ 的 new 和 delete

New 的不同使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <new>

int main()
{
//1 抛异常new
int* p1;
try
{
p1 = new int(2);//分配内存并初始化
}
catch (const std::bad_alloc& e)//判断是否抛异常
{

}

//2 不抛异常new
int* p2 = new (std::nothrow)int();//不抛异常
//3 开辟常量内存
const int* p3 = new const int(40);//开辟一个常量

//4 定位new
int data = 0;
int* p4 = new(&data) int(50);//在指定地址内存初始化,本身并不开辟内存,只负责初始化
delete p1;
delete p2;
delete p3;
delete p4;

return 0;
}

1. malloc 与 new 的区别

  1. malloc 按字节开辟内存的;new 开辟内存时需要指定类型;
  2. malloc 开辟内存返回的都是 void * ,new 相当于运算符重载函数,返回值自动转为指定的类型的指针。
  3. malloc 只负责开辟内存空间,new 不仅仅也有 malloc 功能,还可以进行数据的初始化。
  4. malloc 开辟内存失败返回 nullptr 指针;new 抛出的是 bad_alloc 类型的异常。
  5. malloc 开辟单个元素内存与数组内存是一样的,都是给字节数;new开辟时对单个元素内存后面不需要[],而数组需要 []并给上元素个数。

2. free 和 delete 的区别:

  1. free 不管释放单个元素内存还是数组内存,只需要传入内存的起始地址即可。
  2. delete 释放单个元素内存,不需要加中括号,但释放数据内存时需要加中括号。
  3. delete 执行其实有两步,先调用析构,再释放;free 只有一步。

3. 解析

代码:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int main()
{
int* p = new int;
delete p;
p = nullptr;

return 0;
}

反汇编:

image.png

new 与 delete 其本质也是函数的调用:运算符重载 new  delete

1
2
new -> operator new
delete -> operator delete

4. 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
using namespace std;

// new:
// 先调用 operator 开辟内存空间
// 然后调用对象的构造函数
// operator new 实现
void* operator new(size_t size)
{
// 开辟
void* p = malloc(size);
// 判断
if (p == nullptr)
{
throw bad_alloc();
}

cout << "operator new addr:" << p << endl;

return p;
}

// operator new[] 实现
void* operator new[](size_t size)
{
// 开辟
void* p = malloc(size);
// 判断
if (p == nullptr)
{
throw bad_alloc();
}

cout << "operator new[] addr:" << p << endl;

return p;
}

// delete:
// 调用 p 指向对象的析构函数
// 再调用 operator delete 释放空间
// operator delete 实现
void operator delete(void* ptr)
{
cout << "operator delete addr: " << ptr << endl;
free(ptr);
}

// operator delete[] 实现
void operator delete[](void* ptr)
{
cout << "operator delete[] addr: " << ptr << endl;
free(ptr);
}

// 使用
int main()
{
int* p = new int(5);
delete p;
p = nullptr;

p = new int[5];
delete[] p;
p = nullptr;

return 0;
}

image.png

5. new 和delete 能够混用吗?

C++为什么区分单个元素和数组的内存分配和释放呢?

情况1:int类型下将其混用

1
2
3
4
5
int *p = new int;
delete[]p;

int *q = new int[10];
delete q;

能够混用。对于整型来说,没有构造函数与析构函数,针对于 int 类型,new 与 delete 功能只剩下 malloc 与 free 功能,可以将其混用。

情况2:类类型下将其混用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
public:
Test(int data = 10):ptr(new int(data))
{
cout << "Test()" << endl;
}
~Test()
{
delete ptr;
cout << "~Test()" << endl;
}
private:
int *ptr;
};
  • 单个元素与 delete[] 混用:
1
2
Test *p1 = new Test();
delete[]p1;

报错程序

程序崩溃。

  • 数组与 delete 进行混用
1
2
Test *p2 = new Test[5];
delete p2;

程序崩溃。

分析:

正常情况下,每一个 Test 对象有一个整型成员变量,这里分配了 5 个 Test 对象。delete 时先调用析构函数,this 指针将正确的对象的地址传入析构函数中,加了 [] 表示有好几个对象,有一个数组其中每一个对象都要进行析构。但 delete 真正执行指令时,底层是 malloc 按字节开辟,并不知道是否开辟了 5 个 Test 对象的数组,因此还要再多开辟一个 4 字节来存储对象的个数,假设它的地址是 0x100;但是 new 完之后 p2 返回的地址是 0x104 地址,当我们执行 delete[] 时,会到 4 字节来取一下对象的个数,将知道了是 5 个并将这块内存平均分为 5 份,将其每一份对象起始地址传给相应的析构函数,正常析构,最后将 0x100 开始的 4 字节也释放。

而 p2 出错是给用户返回的存对象开始的起始地址,delete p2 认为 p2 只是指向了一个对象,只将 Test[0] 对象析构,直接从 0x104 free(p2),但底层实际是从 0x100 开辟的,因此崩溃。

而 p1 出错:p1 只是单个元素,从 0x104 开始开辟内存,但是 delete[] p1,里面并没有那么多元素,最后还释放了 4 个字节的存储对象个数的内存(即从 0x100 释放)因此崩溃。

image.png


02.C++ 基础精讲
http://example.com/2023/09/21/02.C++ 基础部分/02.C++ 基础精讲/
Author
Yakumo
Posted on
September 21, 2023
Licensed under