类的六个默认成员函数

构造函数

构造函数是一个特殊的成员函数,名字与类名相同,无返回值,可以重载,创建对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次,其主要任务是初始化对象

使用构造函数

C++提供了显式调用和隐式调用两种使用构造函数来初始化对象的方式
注意:如果通过隐式调用默认构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}
private:
int _a;
string _s;
};

int main() {
S s1 = S(3, "Ran"); // 显示调用
S s2(3, "ran"); // 隐式调用
S s3; // S s3();为函数声明
return 0;
}

默认构造函数

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且只能有一个
若未显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,主要作用是调用自定类型成员的默认构造,一旦用户显式定义编译器将不再生成

初始化列表

构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,在初始化列表中,而构造函数体内可以多次赋值
声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

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
class A {
public:
A(int a)
: _a(a) {
}
private:
int _a;
};

class B {
public:
// 冒号开始,以逗号分隔的数据成员列表,
// 每个成员变量后面跟一个放在括号中的初始值或表达式
B(int a, int ref)
: _aobj(a)
, _ref(ref)
, _n(10) {
}
private:
/* 引用成员变量、const成员变量、类类型成员(该类没有默认构造函数)
必须放在初始化列表位置进行初始化 */
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};

单参构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class S {
public:
S(int s = 1) {
_s = s;
}
private:
int _s;
};

int main() {
S obj;
obj = 1999; // 实际编译器会构造一个无名对象,最后用无名对象给obj进行赋值
return 0;
}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换

1
2
3
explicit S(int s = 1) {
_s = s;
}

C++11 成员初始化

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。即,如果初始化列表和声明的时候都有初始化参数,会优先使用初始化列表初始化

C++11 委派构造函数

通过委派其他构造函数,使多构造函数的类编写更容易

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
// 委派构造函数将构造的任务委派给目标构造函数
class Info {
public:
// 目标构造函数
Info()
: _a(0)
, _c('a') {
InitRSet();
}

// 委派构造函数
Info(int a)
: Info() {
_a = a;
}

// 委派构造函数
Info(char c)
: Info() {
_c = c;
}
private:
void InitRSet() {
//初始化其他变量
}
int _a;
char _c;
// ...
};

构造函数不能同时“委派”和使用初始化列表

拷贝构造函数

本质是构造函数的一个重载形式,参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
若未显示定义,编译器自动生成默认拷贝构造函数,默认的拷贝构造函数为浅拷贝
若显示定义拷贝构造,则编译器不会生成默认构造函数(拷贝构造是构造函数的重载)

析构函数

无参数无返回值,一个对象只有一个析构函数
若未显式定义,系统会自动生成默认的析构函数,对象在销毁时会自动调用析构函数,并对自定类型成员调用它的析构函数,完成类的一些资源清理工作
如果是在栈上创建多个对象,则最后创建的对象最先被删除,最先创建的对象最后被删除
注意:申请空间时候必须自己写析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}

~S() {
cout << "~S()" << endl;
}
private:
int _a;
string _s;
};

int main() {
{ // 如果没有大括号,代码块将为整个main(),
// 仅当main()执行完毕调析构,在窗口环境中可能无法看到~S()
S s = S();
}
return 0;
}

输出结果 ~S()
某些编译器可能输出两个 ~S()

C++标准允许编译器使用两种方式来执行S s = S();

  1. 第一种等价于S s;创建一个对象,执行一次析构
  2. 第二种会创造一个匿名临时对象,然后将匿名对象复制到s中,创建两个对象,执行两次析构

所以尽量使用S s;这种隐式构造,通常效率更高

运算符重载

运算符重载是一种形式的C++多态

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的用户定义参数,防止用户为内置类型重载运算符。
  • 必须遵守语法规则,如,不可将%重载成一个操作数。
  • .* :: sizeof ?: . typeid const_cast dynamic_cast reinterpret_cast static_cast 不可重载

一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的浅拷贝
如果需要自己写赋值运算符重载,需要检查是否自己给自己赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 运算符重载演示
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}

S& operator+(S& s) {
_a += s._a;
return *this;
}
private:
int _a;
string _s;
};

int main() {
S a, b, c;
a = a + b + c;
// a = a.operator+(b).operator+(c); 和上面代码等价
return 0;
}

取地址及const取地址操作符重载

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
const对象不能调用非const成员函数
const成员函数内不能调用其它的非const成员函数

取地址及const取地址操作符重载一般不用重新定义,编译器默认会生成
Type* operator&() { return this; }
const Type* operator&() const { return this; }

内联函数

编译时,编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率,但是占用更多内存。

  • 当执行函数代码时间比处理函数调用机制时间长时,则节省的时间比例不高,不必声明为内联。当函数代码少,并且函数经常被调用,声明为内联可以提升程序运行效率
  • inline对于编译器而言只是一个建议,编译器会自动优化,如果函数体内有循环/递归等,编译器优化时会忽略掉内联
  • inline在类外定义时,只需在类实现部分中使用inline限定符,必须在每个使用它们的文件中都定义,防止链接错误,因为inline被展开,就没有函数地址了,链接就会找不到。所以直接将定义放在头文件中最简单

由于宏定义的缺点,在C++中我们可以采用内联函数和const替换宏的函数和常量定义