C++手稿集锦

哪些变量会自动初始化?

在C语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化而拥有不可预测的值(C++默认初始化策略也是如此)。 C++保证了所有对象与对象成员都会初始化,无论是否写了圆括号或者是否写了参数列表,但其中基本数据类型的初始化还得依赖于构造函数中手动初始化。

成员变量分为成员对象和内置类型成员,其中成员对象总是会被初始化的。我们通常会在构造函数中手动初始化所有内置类型的成员。例如:

1
2
3
4
5
class A{
public:
int v;
A(): v(0);
};

参考链接

静态变量实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将构造函数私有化,并提供获取单例的方法。 此后还需禁止复制构造函数、禁止赋值运算符。
class CPerson{
private:
static CPerson* p;
CPerson(){};
CPerson(CPerson&);
const CPerson& operator= (const CPerson&);
public:
static Person* instance(){
return p ? p : (p = new P());
}
};
CPerson* CPerson::p = NULL; // 静态成员必须在声明类的文件中进行声明(通常会初始化),否则链接错。

对象的生命周期,构造与析构

对象实例化

可以直接定义对象变量,在栈中分配并初始化对象;也可以定义对象指针,从堆中分配空间并初始化对象。

1
2
3
CPerson p1;
CPerson p2(2);
CPerson* p3 = new CPerson(3);

析构函数

类的声明中,签名为~CPerson()的方法称为析构函数。析构函数没有参数和返回值。当对象生命周期结束时被调用,通常用来释放资源。一个类只能由一个析构函数。析构函数与构造函数类似,用户不指定时编译器会生成一个缺省的析构(构造)函数, 缺省的析构(构造)函数是空函数。

构造析构顺序

对象的构造过程中,首先完成父类的构造函数,再完成成员对象的构造,最后调用当前类的构造函数:

  1. 构造父类的对象。在此过程中对象的动态类型是仍然是父类。
  2. 构造对象属性。它们实例化的顺序只取决于在类中声明的顺序,与初始化列表中的顺序无关。
  3. 调用构造函数。在这里完成当前类指定的构造过程。

对象的析构过程恰好相反,首先调用当前类的析构函数,然后析构对象属性,最后析构父类对象。

对象指针数组

1
CPerson *p[3] = {new CPerson(1), new CPerson(2)};

复制构造函数

类的声明中,签名为CPerson(CPerson&)的方法称为复制构造函数,用来从一个已存在的对象复制生成一个新的对象。 在如下三种情况下会被调用:

  1. 用一个对象初始化另一个对象时。例如:

    1
    2
    CPerson p2(p1);
    CPerson p2 = p1;
  2. 对象作为参数传递时。例如:

    1
    void func(A a){}
  3. 对象作为返回值时。例如:

    1
    A func(){ A a; return a;}

只有一个参数的复制构造函数可以被称为转换构造函数。当需要类型转换时,会被调用:

1
CPerson = 2;    // CPerson(int) called

注意区分赋值和初始化:对象变量间赋值不会调用复制构造函数。赋值只会按位拷贝对象所在的内存。当然你也可以重载operator=来改变它的行为。

对象生命周期

如下程序解释了对象的声明周期何时开始,以及何时结束。涉及到了:全局对象、静态对象、栈中的对象、堆中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CPerson p1;             // main执行前,构造函数被调用
void func(){
static CPerson p2; // func第一次执行前,构造函数被调用
CPerson p3; // p3的构造函数被调用
// func结束时,p3的析构函数被调用
}
int main(){
CPerson p4, *p5; // 调用p4的构造函数
func();
p5 = new CPerson; // 调用p5的构造函数
delete p5; // 调用p5的析构函数
// main结束时,p4的析构函数被调用
}
// 程序结束前,p1, p2的析构函数被调用

栈对象和堆对象的创建方式

C++中,内存划分为三个逻辑区域:堆、栈和静态存储区,对象分为堆对象,栈对象以及静态对象(构造之后直到main程序结束才调用析构函数将其销毁,不管是local还是非local static对象)。而类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;两者都要执行构造函数。

  1. 静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数,内存空间自动释放

  2. 动态建立类对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数,内存空间需要手动释放。

  3. 注意点

  1. 栈对象和堆对象区别和优劣 : 栈对象的创建速度一般较堆对象快,它仅仅需要移动栈顶指针就可以被创建,递归函数中最好不要使用栈对象以避免栈溢出;程序员对堆对象的生命具有完全的控制权。比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。

  2. 如何限制类对象只能在堆上建立? 编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存(如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数)。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。

  3. 构造函数设置为私有会限制继承。如果一个类不打算作为基类,可以将其析构函数声明为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 ; //防止使用悬挂指针
    >

>

  1. 只要禁用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. CPP进程的地址空间分配-堆栈空间

引用和指针

  1. 引用定义一个变量的别名,是一种隐式的指针,对它的操作都会被解释为对它引用的对象的操作。 引用不占用栈空间,因为编译器知道它的地址。但作为参数传递引用时,会把指针放在参数栈中。为了在函数中修改传入的参数,可以把函数参数声明为引用。引用作为函数的返回值,一般是为了在函数外部修改内部变量。

常量指针与指针常量

常量指针指向地方的内容不可改变,指针常量指向的地方不可改变

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义整数
int n = 1, m=2;

// 常量指针
const int * p = &n;
*p = 5; // 编译错,内容不可变

// 指针常量
int * const p = &n;
p = &m; // 编译错,地址不可变

// 指针和指向的对象都是常量
const int * const p = &n;

指针数组vs数组指针

1
2
3
4
5
// 指针数组:数组的长度为8,数组的每一项都是类型为int*的指针
int* arr[8];

// 数组指针:一个指向长度为8的数组的指针
int (*arr)[8];

函数指针

函数指针通常用来进行传参,借此实现动态的策略。传参时可以用函数名,也可以用函数指针。而函数指针需要用函数名来初始化。

1
2
3
4
5
6
7
8
9
10
11
12
int sum(int a, int b)j{
return a+b;
}
void wrapper(int a, int b, int (*p)(int, int)){
cout<<p(a, b)<<endl;
}

int main(){
wrapper(2, 3, sum);
int (*p)(int, int) = sum;
wrapper(2, 3, p);
}

C++ 智能指针与裸指针

我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数,析构函数会自动释放资源。

为什么要用智能指针?

  1. 难以区分裸指针指向的是单个对象还是一个数组,也无法确定到底是用delete(销毁单个对象)还是delete[]销毁一个数组;
  2. 使用完指针之后无法判断是否应该销毁指针,因为无法判断指针是否“拥有”指向的对象,也无法确定销毁指针的方式;
  3. 遗漏销毁指正可能导致内存泄露,而销毁多次则会导致未定义行为。

四种智能指针

  1. 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))
  2. 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