哪些变量会自动初始化?
在C语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化而拥有不可预测的值(C++默认初始化策略也是如此)。 C++保证了所有对象与对象成员都会初始化,无论是否写了圆括号或者是否写了参数列表,但其中基本数据类型的初始化还得依赖于构造函数中手动初始化。
成员变量分为成员对象和内置类型成员,其中成员对象总是会被初始化的。我们通常会在构造函数中手动初始化所有内置类型的成员。例如:
1 | class A{ |
静态变量实现单例模式
1 | // 将构造函数私有化,并提供获取单例的方法。 此后还需禁止复制构造函数、禁止赋值运算符。 |
对象的生命周期,构造与析构
对象实例化
可以直接定义对象变量,在栈中分配并初始化对象;也可以定义对象指针,从堆中分配空间并初始化对象。
1 | CPerson p1; |
析构函数
类的声明中,签名为~CPerson()
的方法称为析构函数。析构函数没有参数和返回值。当对象生命周期结束时被调用,通常用来释放资源。一个类只能由一个析构函数。析构函数与构造函数类似,用户不指定时编译器会生成一个缺省的析构(构造)函数, 缺省的析构(构造)函数是空函数。
构造析构顺序
对象的构造过程中,首先完成父类的构造函数,再完成成员对象的构造,最后调用当前类的构造函数:
- 构造父类的对象。在此过程中对象的动态类型是仍然是父类。
- 构造对象属性。它们实例化的顺序只取决于在类中声明的顺序,与初始化列表中的顺序无关。
- 调用构造函数。在这里完成当前类指定的构造过程。
对象的析构过程恰好相反,首先调用当前类的析构函数,然后析构对象属性,最后析构父类对象。
对象指针数组
1 | CPerson *p[3] = {new CPerson(1), new CPerson(2)}; |
复制构造函数
类的声明中,签名为CPerson(CPerson&)
的方法称为复制构造函数,用来从一个已存在的对象复制生成一个新的对象。 在如下三种情况下会被调用:
用一个对象初始化另一个对象时。例如:
1
2CPerson p2(p1);
CPerson p2 = p1;对象作为参数传递时。例如:
1
void func(A a){}
对象作为返回值时。例如:
1
A func(){ A a; return a;}
只有一个参数的复制构造函数可以被称为转换构造函数。当需要类型转换时,会被调用:
1 | CPerson = 2; // CPerson(int) called |
注意区分赋值和初始化:对象变量间赋值不会调用复制构造函数。赋值只会按位拷贝对象所在的内存。当然你也可以重载
operator=
来改变它的行为。
对象生命周期
如下程序解释了对象的声明周期何时开始,以及何时结束。涉及到了:全局对象、静态对象、栈中的对象、堆中的对象。
1 | CPerson p1; // main执行前,构造函数被调用 |
栈对象和堆对象的创建方式
C++中,内存划分为三个逻辑区域:堆、栈和静态存储区,对象分为堆对象,栈对象以及静态对象(构造之后直到main程序结束才调用析构函数将其销毁,不管是local还是非local static对象)。而类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;两者都要执行构造函数。
静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数,内存空间自动释放
。
动态建立类对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数,内存空间需要手动释放。
注意点:
栈对象和堆对象区别和优劣 : 栈对象的创建速度一般较堆对象快,它仅仅需要移动栈顶指针就可以被创建,递归函数中最好不要使用栈对象以避免栈溢出;程序员对堆对象的生命具有完全的控制权。比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。
如何限制类对象只能在堆上建立? 编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存(如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数)。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。
构造函数设置为私有会限制继承。如果一个类不打算作为基类,可以将其析构函数声明为private。为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 > class NoStackObject {
> protected:
> NoStackObject() { }
> ~NoStackObject() { }
> public:
> static NoStackObject* creatInstance() {
> return new NoStackObject() ;//调用保护的构造函数
> }
> void destroy() {
> delete this ;//调用保护的析构函数
> }
> };
> NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
> hash_ptr->destroy() ;
> hash_ptr = NULL ; //防止使用悬挂指针
>
>
只要禁用new运算符就可以实现类对象只能建立在栈上: 你不能影响new operator的能力(因为那是C++语言内建的),但是new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上;如果也想禁止堆对象数组,可以把operator new[]和operator delete[]也声明为private。
1
2
3
4
5
6
7
8
9
10 > class A
> {
> private:
> void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
> void operator delete(void* ptr){} // 重载了new就需要重载delete
> public:
> A(){}
> ~A(){}
> };
>
>
引用和指针
- 引用定义一个变量的别名,是一种隐式的指针,对它的操作都会被解释为对它引用的对象的操作。 引用不占用栈空间,因为编译器知道它的地址。但作为参数传递引用时,会把指针放在参数栈中。为了在函数中修改传入的参数,可以把函数参数声明为引用。引用作为函数的返回值,一般是为了在函数外部修改内部变量。
常量指针与指针常量
常量指针指向地方的内容不可改变,指针常量指向的地方不可改变
1 | // 定义整数 |
指针数组vs数组指针
1 | // 指针数组:数组的长度为8,数组的每一项都是类型为int*的指针 |
函数指针
函数指针通常用来进行传参,借此实现动态的策略。传参时可以用函数名,也可以用函数指针。而函数指针需要用函数名来初始化。
1 | int sum(int a, int b)j{ |
C++ 智能指针与裸指针
我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数,析构函数会自动释放资源。
为什么要用智能指针?
- 难以区分裸指针指向的是单个对象还是一个数组,也无法确定到底是用delete(销毁单个对象)还是delete[]销毁一个数组;
- 使用完指针之后无法判断是否应该销毁指针,因为无法判断指针是否“拥有”指向的对象,也无法确定销毁指针的方式;
- 遗漏销毁指正可能导致内存泄露,而销毁多次则会导致未定义行为。
四种智能指针
- std::auto_ptr和std::unique_ptr:auto_ptr是c++ 98遗留的关键字,已经不建议使用,auto_ptr的功能都可以由unique_ptr更加高效的做到。
- auto_ptr智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。
- auto_ptr指针当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL)。
- unique_ptr指针永远“拥有”其指向的对象,unique_ptr是一个move-only类型,一个unique_ptr指针无法被复制,只能将“所有权”在两个unique_ptr指针之间转移,转移完成后源unique_ptr将被设为null。unique_ptr默认的销毁方式是通过对unique_ptr中的裸指针进行delete操作,它可以无缝地转换成shared_ptr。不能使用两个unique_ptr智能指针赋值操作,应该使用std::move,例如foo(std::move(ptest))
- std::shared_ptr和std::weak_ptr:
- shared_ptr使用计数机制来表明资源被几个指针共享,可以自动管理对象的生命周期和GC,shared_ptr的引用计数增减是原子操作
- 一个对象可以被多个shared_ptr指向和访问,这些shared_ptr类型的指针共同享有该对象的所有权,当最后一个指向该对象的shared_ptr生命周期结束的时候,对象被销毁
- shared_ptr的构造将引用计数加1,销毁的时候引用计数减1,而赋值则将源指针引用计数加1,目标指针引用计数减1,例如P1=P2,P1指向对象的引用计数减1,P2指向对象的引用计数加1。当引用计数减1之后为0的时候,shared_ptr将会销毁指向的对象。
- 由于control_block的存在,shared_ptr的size通常是2倍裸指针或unique_ptr的大小
- weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。不能通过weak_ptr直接访问对象的方法,应该先把它转化为shared_ptr