C++杂谈

  1. 变量的声明与定义
  2. 指针域引用
  3. new与malloc
  4. 结构体与联合
  5. 初始化与赋值

变量的声明与定义

变量与对象其实只是叫法不同,一般可以交换使用。下面均以对象称之。

对象是指一块能存储数据并具有某种类型的内存空间。对象的定义的基本格式是:类型说明符,随后紧跟由一个或多个对象名组成的列表,其中对象名以逗号分隔,每个对象的类型都由类型说明符指定,定义时还可以为对象赋初值。
简单的说,定义负责申请一块内存空间以存放对象,申请的大小取决于定义的类型,其中存放的数据可以定义时进行指定,也可以不指定,如果不指定则根据对象的定义位置进行初始化与否。(局部对象不默认初始化,全局对象和静态对象会默认初始化)

声明与定义有类似的地方,也有不同的地方。声明的任务主要是使得对象的名字为程序所知。它也有做和定义相同的事情:规定了对象的类型和名字,此外定义还负责申请内存空间,也可能会为对象赋一个初值。
如果想声明一个对象而不是定义它,就在变量名前面加上关键字extern,而且不要显式的初始化变量(如果赋一个初始值,则抵消了extern的作用):

1
2
3
extern int i;    //声明而不是定义i
int j; //声明并定义j
extern int k = 0; //定义k而非声明

对象只可以定义一次,但是可以多次声明。这也很好理解,在不同的文件中告诉其这个对象的名字可以使用;定义只分配了一块内存空间,如果多次定义则是定义了不同的对象(作用域不同),也有可能报错(同一个作用域)。比如在多个文件中使用同一个对象,则必须把对象的声明与定义分离,此时变量的定义必须出现并且这能出现在一个文件中,而其他用到改变对象的文件必须对其进行声明,决不能重复定义。

指针与引用

引用

引用不是对象(不占空间),它只是为对象起了另外一个名字,引用名(在编译器看来)其实就是对象名,它们是同一个东西,定义了一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的。在初始化对象时,初始值会被拷贝到新建的对象中。但是在定义引用时,程序把对象和它的引用绑定在一起,而不是将对象的初始值拷贝给引用。一旦初始化完成,引用将和其对象一直绑定在一起,不允许再绑定到其他对象上。也因为这个原因,引用必须进行初始化。

因为引用不是一个对象,所以不能定义引用的引用。

指针

指针是指向另外一种类型的复合类型,和引用一样它实现了对其他对象的间接访问。
指针存放的是某个对象的地址,可以通过取地址符&来获取对象的地址。

1
2
int ival = 42;
int *p = &ival; //定义了一个指针变量p,它的空间所存放的是ival对象的地址

指针的值可以是下列四种状态之一:

  1. 指向一个对象。
  2. 指向一个对象的下一个位置。
  3. 空指针,意味着指针没指向任何对象。
  4. 无效指针,上述情况之外的其他值。

对指针对象进行解引用,获得的就是其指向的对象,所以对指针解引用后的操作就是对对象的操作:

1
*p = 0;    //ival的值变为0

C++11标准下,空指针最好使用nullptr而避免使用NULL

区别

  • 引用必须初始化,其初始化值必须是一个对象。指针可以不初始化。
  • 引用一旦初始化,中途不允许改变。指针空间中存放的是指向的对象的地址,可以给指针赋一个新的地址,从而可以指向一个新的对象(不讨论const限制)。
  • 引用不是一个对象,不可以取其地址。指针是一个实实在在存在的实体,有内存空间和地址。
  • 引用是其绑定对象的别名,它们是一个东西。指针是另外一块空间,其存储的是它指向对象的地址,可以解引用间接访问,它们实质是不同的。

new 与malloc

内存申请的位置

new与malloc都可以动态的分配内存,但是malloc都是在堆上分配内存,(堆是操作系统为进程划分的一块特殊的区域,位于未初始化数据段的上方。)new在自由存储区上分配内存(自由存储区是一种抽象的概念,只要是new分配的内存,都称为位于自由存储区)。这两者有什么区别呢?自由存储区可以是堆,也可以是静态存储区,这取决于operator new的具体实现。

返回类型安全性

new操作符内存分配成功时,返回的是对象类型的指针,类型与对象严格匹配,无法进行类型转换。所以说new是类型安全的操作符。
malloc返回类型是void*,需要通过强制类型转换为想要的类型。

内存分配失败时的返回值

当没有足够的空间或者分配中途被信号打断时,都会导致分配失败。malloc分配失败时会返回NULL指针;new分配失败时会抛出bad_alloc异常,而不是返回NULL。如果不想抛出异常,可以指定no throw:new (nothrow) classname

是否需要指定内存大小

使用new操作符不需要指定对象的内存大小,因为编译器会根据对象类型计算所需大小,而malloc则需要指定内存分配的大小

1
2
3
class A{...};
A a1 = new(A);
A a2 = (A*)malloc(sizeof(A));

是否会调用构造函数,释放调用析构函数?

在定义内置类型对象时,new与malloc是没有区别的。但是在定义自定义类型的对象时就有所不同了。
使用malloc只会分配一块内存,不会进行初始化操作,自然也就不会调用类的构造函数。使用free释放时也只是进行该内存区域的释放,不会调用析构函数进行进一步的释放。如果类对象有另外新分配的内存,使用free将不会释放这块区域,导致内存泄漏。
使用new则不同,它会执行三个步骤:

  1. 分配一块原始的,未命名的内存以供对象使用。
  2. 调用类的构造函数将该内存区域初始化。
  3. 将分配好的内存区域的地址返回。

使用delete时执行两个步骤:

  1. 调用对象析构函数。
  2. 对该内存空间进行释放。

是否可以相互调用

operator new是基于malloc实现的,但是malloc不可以调用new

直观的内存扩充

使用malloc过程中,如果发现内存不足,则调用realloc来重新分配内存实现内存的扩充,realloc会首先判断此时指针所指位置是否还有足够的连续空间,如果有,原地扩大可分配内存空间。返回原来内存的地址;如果没有,则按照指定大小重新分配一块内存空间,将旧空间内的数据依次拷贝到新分配的空间,释放旧空间,返回新内存地址。

new则没有这么直观的内存扩充。

结构体与联合

结构体就是把一些数据项组合在一起的数据结构,每个成员依次存储。联合是所有的成员都从0偏移地址开始存储,每个成员都重叠在一起,也就是说,在某一时刻,只有一个成员真正存储在该地址中。它可以把同一个数据解释成不同的东西。

联合一般用来节省空间,因为有些数据项是不可能同时出现的,此时如果用结构体存储显得颇为浪费。

初始化与赋值

1
2
3
4
5
class A{...};
A a1; //1
A a2 = a1; //2
A a3; //3
a3 = a1; //4

从这个典型的例子入手:

  1. a1即新创建的一个对象,其初始值由默认构造函数进行指定。
  2. 创建a2对象时有所不同,它的初始值由a1拷贝而来。此时调用的是拷贝构造函数,虽然用到了等号。
  3. 创建a3,由构造函数负责初始化。
  4. 将a1的值赋值给a3。此时调用的是拷贝构造函数。

还有一个例子就是使用构造函数的初始化列表和不使用初始化列表:

1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
A(int _a,int _b,char _c):a(_a),b(_b),c(_c){ } //1
A(int _a,int _b ,char _c){ //2
a = _a;
b = _b;
c = _c;
}
private:
int a,b;
char c;
}

这两个效果相同,但是效率还有有差别的。实际上,在调用构造函数时,首先会执行对象的初始化,然后才会进入函数体。在上述两个构造函数中:

  1. 使用初始化列表,调用1时对象成员还没有初始化,先执行初始化列表进行初始化,然后进入函数体,此时函数体为空,即不进行任何操作。
  2. 没有使用初始化列表,调用2时,首先执行对象成员的默认初始化,即给赋一个初始值,然后执行函数体内的赋值,这样来看,刚才的初始化就没有任何意义。效率显然不如1高。

看下验证结果:

简单的说:初始化发生在对象还没有初始值时给其赋一个初始值,不管有没有用到等号,只要对象是从无到有,就是初始化。赋值就是对象有初始值了,重新给赋了一个新的值。即原来就有,只是改了一个值


参考资料:
《C++primer》
《C专家编程》
细说new与malloc的10点区别