C++语法之封装、构造函数、析构函数

本文首发于个人博客

封装

  • 成员变量私有化,提供公共的getter和setter给外界去访问成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
int age;

public:
void setAge(int age){
this->age = age;
}

int getAge(){
return this->age;
}

};

int main(){
Person person;
person.setAge(10);
cout << person.getAge() << endl;
}

堆空间

  • 在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存

  • 堆空间的申请\释放

    • malloc \ free
    • new \ delete
    • new [] \ delete []
  • 注意

    • 申请堆空间成功后,会返回那一段内存空间的地址
    • 申请和释放必须是1对1的关系,不然可能会存在内存泄露
  • 现在的很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在

    • 利:提高开发效率,避免内存使用不当或泄露
    • 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手

例如开盘int类型的空间,使用完之后销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);

int *p2 = new int;
*p2 = 20;
delete p2;

int *p3 = new int[3];
*p = 10;
*(p+1) = 20;
*(p+2) = 30;
delete [] (p3);

堆空间的初始化

memset

memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法

如下所示

1
2
3
4
5
 Person person;
person.age = 10;
person.height = 199;
//从person的地址开始,每个字节都赋值为0
memset(&person, 0, sizeof(person));

初始化

1
2
3
int *p1 = (int *)malloc(sizeof(int)); //*p1 未初始化
int *p2 = (int *)malloc(sizeof(int));
memset(p2, 0, sizeof(int));//将 *p2 的每一个字节都初始化为0

如下几种方式

1
2
3
4
5
6
7
int *p1 = new int;           //未初始化
int *p2 = new int(); //被初始化为0
int *p3 = new int(5); //被初始化为5
int *p4 = new int[3]; //数组元素未被初始化
int *p5 = new int[3](); //3个数组元素都被初始化0
int *p6 = new int[3]{}; //3个数组元素都被初始化0
int *p7 = new int[3]{5}; //数组首元素被初始化为5,其他元素被初始化为0

构造函数(Constructor)

  • 构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作

特点

  • 函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数
  • 一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象

注意

  • 通过malloc分配的对象不会调用构造函数
  • 一个广为流传的、很多教程\书籍都推崇的错误结论:
    • 默认情况下,编译器会为每一个类生成空的无参的构造函数
    • 正确理解:在某些特定的情况下,编译器才会为类生成空的无参的构造函数

比如我们自己写2个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
public:
int age;

Person(){
cout << "Person()" << endl;
}

Person(int age){
cout << "Person(int age))" << endl;
}
};

在不同的空间调用的时候,如下区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 全局区
Person p1; //调用Person()
Person p2(); //这是一个函数,函数名是p2,返回值类型是Person,无参
Person p3(18); //调用 Person(int)

int main(){
//栈空间
Person p4; //调用Person()
Person p5(); //这是一个函数,函数名是p5,返回值类型是Person,无参
Person p6(18); //调用 Person(int)


//堆空间
Person *p7 = new Person; //调用Person()
Person *p8 = new Person(); //调用Person()
Person *p9 = new Person(20); //调用 Person(int)
}

构造函数的互相调用

下面代码中有2个构造函数,Person()是可以调用Person(int age, int height):m_age(age),m_height(height)的。

1
2
3
4
5
6
7
8
9
class Person{
public:
int m_age;
int m_height;
Person():Person(10,20){}
Person(int age, int height):m_age(age),m_height(height){

}
};

父类的构造函数

  • 子类的构造函数默认会调用父类的无参构造函数
  • 如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
  • 如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数

析构函数

  • 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作

特点

  • 函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数

注意

  • 通过malloc分配的对象free的时候不会调用析构函数
  • 构造函数、析构函数要声明为public,才能被外界正常使用

例如下面的代码

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
class Cat{
public:
int age;
Cat(){
cout << "Cat()" << endl;
}

~Cat(){
cout << "~Cat()" << endl;
}
};


class Person{
public:
int age;
Cat *cat;
Person(){
this->cat = new Cat();
cout << "Person()" << endl;
}

~Person(){
cout << "~Person()" << endl;
}
};

int main(){
{
Person person;
}
return 0;
}

输出

1
2
3
Cat()
Person()
~Person()

当person销毁的时候,其持有的cat并没有销毁。

原因

  • 当person销毁的时候,其指向cat对象的指针销毁了,但是堆空间的cat对象依然存在,就会有内存泄露。所以需要在析构函数里面来释放掉。类似的析构函数在许多其他语言底层也是应用广泛,例如Objective-C的源码中,大量使用析构函数。

代码改成如下所示:

1
2
3
4
~Person(){
delete cat;
cout << "~Person()" << endl;
}

输出

1
2
3
4
Cat()
Person()
~Cat()
~Person()

可知,cat对象才真正销毁。