目录
12.1 隐式类型转换,连续构造+拷贝构造->优化为直接构造
12.2 一个表达式中,连续构造+拷贝构造->优化为直接构造
12.3 一个表达式中,连续拷贝构造+拷贝构造->优化为一个拷贝构造
类的基本思想是数据抽象( data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
类要想实现数据抽象和封装,需要首先定义一个抽象数据类型(abstract data type)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。
1. 类的定义
class(或struct) 类名
{
// 类体
};
类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
成员变量最好加个前缀或后缀标识,以便和函数形参作区分。
class Date
{
public:
void Init(int year, int month, int day)
{
year = year; // 这里的year是成员变量,还是函数形参?根据局部优先,是函数形参
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
成员变量前加下划线以作标识:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
以上是类的声明和定义全部放在类体中的写法,定义在类内部的函数是隐式的inline函数。
以下是类的声明和定义分离的写法:
Date.h:
class Date
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
Date.cpp:
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
#include "Date.h"
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
2. 封装和访问权限
封装:将对象的属性与方法结合在一起,形成“类”。通过访问权限,隐藏对象的属性和实现细节,仅对外公开接口。
访问限定符:
- public(公有)
- protected(保护)
- private(私有)
说明:
- public修饰的成员在类外可以直接被访问。
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
- protected和private的区别在于继承:基类private成员在派生类中不能被访问;基类protected成员在派生类中可以被访问。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到}即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)。
封装有两个重要的优点:
- 确保用户代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
一旦把数据成员定义成private的,类的作者就可以比较自由地修改数据了。当实现部分改变时,我们只需要检查类的代码本身以确认这次改变有什么影响;换句话说,只要类的接口不变,用户代码就无须改变。如果数据是public的,则所有使用了原来数据成员的代码都可能失效,这时我们必须定位并重写所有依赖于老版本实现的代码,之后才能重新使用该程序。
把数据成员的访问权限设成private还有另外一个好处,这么做能防止由于用户的原因造成数据被破坏。如果我们发现有程序缺陷破坏了对象的状态,则可以在有限的范围内定位缺陷:因为只有实现部分的代码可能产生这样的错误。因此,将查错限制在有限范围内将能极大地降低维护代码及修正程序错误的难度。
尽管当类的定义发生改变时无须更改用户代码,但是使用了该类的源文件必须重新编译。
3. class和struct的区别
- class的默认访问权限为private,struct为public(因为struct要兼容C)。
- class的默认继承方式为private,struct为public。
- class可以代替typename定义模板参数,struct不能定义模板参数。
4. C语言和C++的struct的区别
- C语言中struct用来定义结构体,只是一些变量的集合,成员不可以是函数,不支持访问权限的设置,不支持继承和多态。
- C++兼容C,struct自然可以用来定义结构体,也可以用来定义类,支持成员函数,支持访问权限的设置,并且默认访问权限是public,支持继承和多态。
- C语言中结构体名前加上struct才能作为类型名,C++中结构体名直接作为类型名使用。
struct Peo
{
char name[20];
char tele[12];
char sex[5]; // 女 男 保密
int age;
};
// 定义结构体变量
struct Peo p1; // C语言写法
Peo p2; // C++写法
5. 类对象模型
在面向对象的编程中,把用类创建对象的过程称为实例化。类是对对象进行描述的,本身不占用内存空间,实例化出的对象,才会占用内存空间。
成员变量在对象中,成员函数不在对象中。每个对象成员变量是不一样的,需要独立存储;每个对象调用成员函数是一样的,放到共享公共区域(代码段)。
class A1
{
public:
void f1(){}
private:
int _a;
};
// sizeof(A1)=4
class A2
{
public:
void f2(){}
};
// sizeof(A2)=1
class A3
{
};
// sizeof(A3)=1
一个类的大小,实际就是非静态成员变量的数据类型大小之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
考虑继承和多态的情况下,不要忘记算上虚基表指针和虚表指针,它们也是成员变量。
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到对齐数的整数倍的地址处。对齐数是编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
6. this指针
6.1 this指针的引入
定义一个日期类Date:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
return 0;
}
Date类中函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
上述日期类由编译器处理后为:
class Date
{
public:
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(&d1, 2022, 1, 11);
d2.Init(&d2, 2022, 1, 12);
return 0;
}
6.2 this指针的特性
- this指针是常量指针,指针本身不能改变。
- this指针只能在成员函数的内部使用。
- this指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
6.3 this指针的作用
- 当形参和成员变量同名时,可用this指针来区分。
- 在类的非静态成员函数中返回对象本身,可使用return *this。
class Date
{
public:
void Init(int year, int month, int day)
{
// 1. 当形参和成员变量同名时,可用this指针来区分
this->year = year;
this->month = month;
this->day = day;
}
Date& addOneDay() // 日期加1天(不考虑顺延到下个月的情况)
{
this->day++;
// 2. 在类的非静态成员函数中返回对象本身,可使用return *this
return *this;
}
private:
int year;
int month;
int day;
};
6.4 空指针访问成员函数
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 由编译器处理后
/*
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
*/
void func()
{
cout << "func()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* ptr = nullptr;
ptr->Init(2022, 2, 2); // 运行崩溃 有解引用操作 在公共代码区找到Init函数后,对this有解引用操作
ptr->func(); // 正常运行 没有解引用操作 在公共代码区找到func函数后,对this没有解引用操作
(*ptr).func(); // 正常运行 没有解引用操作 在公共代码区找到func函数后,对this没有解引用操作
return 0;
}
7. const成员函数
const修饰类成员函数,实际修饰该成员函数隐含的this指针指向的值,表明在该成员函数中不能对类的任何成员进行修改。
- this指针本身是常量指针,这个const修饰的是this。
- const成员函数的const修饰的是* this,这时this指针既是常量指针,又是指向常量的指针。
class A
{
public:
void Print()
{
cout << _a << endl;
}
// 编译器处理后为
/*
void Print(A* const this)
{
cout << this->_a << endl;
}
*/
private:
int _a = 10;
};
int main()
{
const A a;
a.Print(); // err
return 0;
}
const对象不能调用非const成员函数(构造函数除外)。构造函数一定是非const成员函数,为什么const对象能调用构造函数呢?当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
class A
{
public:
void Print() const
{
cout << _a << endl;
}
// 编译器处理后为
/*
void Print(const A* const this)
{
cout << this->_a << endl;
}
*/
private:
int _a = 10;
};
int main()
{
const A a1;
a1.Print(); // ok
A a2;
a2.Print(); // ok
return 0;
}
内部不改变成员变量的成员函数,最好定义为const成员函数,const对象和普通对象都可以调用。
8. static成员
声明为static的类成员称为类的静态成员。
用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员可用类名::静态成员或者对象.静态成员来访问。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
实现一个类,计算程序中创建出了多少个类对象:
class A
{
public:
A()
{
++_count;
}
A(const A& a)
{
++_count;
}
~A()
{
--_count;
}
static int getACount() // 静态成员函数
{
return _count;
}
private:
static int _count; // 静态成员变量类内声明
};
int A::_count = 0; // 静态成员变量类外定义
int main()
{
A a1, a2;
cout << A::getACount() << endl; // 类名::静态成员函数
A a3(a1);
cout << a2.getACount() << endl; // 对象.静态成员函数
return 0;
}
9. 构造函数
9.1 构造函数的概念
对于以下Date类:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
可以通过Init函数给类对象初始化,但如果每次创建对象时都调用Init函数,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?引入构造函数,用来初始化对象。
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
class Date
{
public:
// 构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
9.2 初始化列表
传统的赋值初始化是指在函数体内用“=”对成员变量进行赋值。对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
class Date
{
public:
// 赋值初始化
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 初始化列表方式初始化
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
1. 引用成员变量(无法赋值,只能初始化)
2. const成员变量(无法赋值,只能初始化)
3. 没有默认构造函数的自定义类型成员(因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数来初始化) - 尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
9.3 构造函数的特性
- 函数名与类名相同。
- 无返回值。
- 构造函数可以有参数,因此可以重载。
- 对象实例化时编译器自动调用对应的构造函数。
- 如果类中没有显式定义构造函数,那么编译器会自动生成无参的默认构造函数。
- 默认生成构造函数对内置类型成员不做处理,对自定义类型成员调用它的默认构造函数。
- 无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数,并且默认构造函数只能有一个。
class Date
{
public:
// 无参构造函数
Date()
: _year(2000)
, _month(8)
, _day(8)
{}
// 带参构造函数
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(1900, 9, 9); // 调用带参构造函数
Date d3(); // 调用无参构造函数的错误写法,对象后面不用加括号,否则就成了函数声明
return 0;
}
class Time
{
// ...
};
class Date
{
private:
// 内置类型 默认生成构造函数对内置类型成员不做处理
int _year;
int _month;
int _day;
// 自定义类型 默认生成构造函数对自定义类型成员调用它的默认构造函数
Time _t;
};
C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Time
{
// ...
};
class Date
{
private:
// 内置类型
int _year = 2023;
int _month = 2;
int _day = 11;
// 自定义类型
Time _t;
};
9.4 委托构造函数
C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数(delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
和其他构造函数一样,一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。
class Date
{
public:
// 非委托构造函数使用对应的实参初始化成员
Date(int year, int month, int day) // 构造函数1
: _year(year)
, _month(month)
, _day(day)
{}
// 其余构造函数全都委托给另一个构造函数
Date() // 构造函数2
: Date(2000, 8, 8)
{}
Date(int year) // 构造函数3
: Date(year, 1, 9)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 10, 1); // 构造函数1
d1.Print(); // 2023-10-1
Date d2; // 构造函数2
d2.Print(); // 2000-8-8
Date d3(2010); // 构造函数3
d3.Print(); // 2010-1-9
return 0;
}
9.5 转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式类型转换机制,有时我们把这种构造函数称作转换构造函数。
class Date
{
public:
// 首先这是普通的构造函数,由于只有一个参数,也是转换构造函数
Date(int year)
: _year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022); // 显然ok 调用带参构造函数
d1 = 2023; // 也ok 将int类型的变量赋值给Date类型的对象,为什么ok?
// 编译器将2023作为参数传递给转换构造函数来创建一个临时对象,再将该临时对象赋值给d1
return 0;
}
explicit修饰构造函数,表示该构造函数是显式的,禁止隐式类型转换。
class Date
{
public:
explicit Date(int year)
: _year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022); // 显然ok 调用带参构造函数
d1 = 2023; // err
return 0;
}
除第一个参数无默认值其余均有默认值的构造函数,也可以是转换构造函数,因为可以只传递一个参数。
class Date
{
public:
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
/*
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
private:
int _year;
int _month;
int _day;
};
int main()
{
// 没有explicit时
Date d1(2022); // ok
d1 = 2023; // ok
// 有explicit时
/*
Date d1(2022); // ok
d1 = 2023; // err
*/
return 0;
}
10. 析构函数
10.1 析构函数的概念
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
10.2 析构函数的特性
- 函数名是在类名前加上字符~。
- 无返回值。
- 析构函数没有参数,因此不可以重载,一个类只能有一个析构函数。
- 对象生命周期结束时编译器自动调用析构函数。
- 如果类中没有显式定义析构函数,那么编译器会自动生成默认的析构函数。
- 默认生成析构函数对内置类型成员不做处理,对自定义类型成员调用它的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
template<typename T>
class Stack
{
public:
// 无参构造函数
Stack()
: _a(nullptr)
, _capacity(0)
, _top(0)
{}
// 其他接口......(我不写了)
// 析构函数
~Stack()
{
delete _a;
_capacity = 0;
_top = 0;
}
private:
T* _a;
int _capacity;
int _top;
//top初始化为0时,表示栈顶下一个,top的数值等于栈中元素个数
//top初始化为-1时,表示栈顶,top的数值等于栈中元素个数-1
};
11. 拷贝构造函数
11.1 拷贝构造函数的概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
11.2 拷贝构造函数的特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 如果类中没有显式定义拷贝构造函数,那么编译器会生成默认的拷贝构造函数。 默认生成拷贝构造函数对内置类型成员作浅拷贝,对自定义类型成员调用它的拷贝构造函数。
- 如果类中没有申请资源时,拷贝构造函数可以不写,直接使用编译器生成的默认拷贝构造函数,比如,Date类;有资源申请时,一定要写,比如Stack类。
- 调用拷贝构造函数的情况:
1)使用已经存在的对象创建新对象
2)函数参数为类类型对象(形参是实参的临时拷贝)
3)函数返回值为类类型对象(传值返回产生临时对象)
11.3 构造函数的调用规则
- 如果显式定义带参构造函数,编译器不会再提供默认无参构造,但是会提供默认拷贝构造。
- 如果显式定义拷贝构造函数,编译器不会再提供其他构造函数。
class Date
{
public:
// 带参构造函数
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 如果显式定义带参构造函数,编译器不会再提供默认无参构造,但是会提供默认拷贝构造
Date d1; // err 没有默认构造函数
Date d2(2000, 10, 1); // ok 调用带参构造函数
Date d3(d2); // ok 调用默认拷贝构造函数
return 0;
}
class Date
{
public:
// 拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 如果显式定义拷贝构造函数,编译器不会再提供其他构造函数
Date d1; // err 没有默认构造函数
Date d2(2000, 10, 1); // err 没有带参构造函数
Date d3(d2); // ok 调用拷贝构造函数
return 0;
}
11.4 深拷贝和浅拷贝
浅拷贝是指按字节序拷贝内存。当成员变量中有指针时,浅拷贝只是拷贝指针的值,并没有新开辟新的内存空间。拷贝后,两个指针指向同一个地址,这会产生一些问题:
- 插入、删除数据会互相影响。
- 对象生命周期结束时,调用两次析构函数,重复释放内存会导致程序崩溃。
深拷贝是指拷贝对象的具体内容,是在堆区开辟出一块新的内存空间存放要拷贝的内容。拷贝后,两个指针指向不同的地址,两个对象之间互不影响。
class MyString
{
public:
MyString()
: _str(nullptr)
{}
/*
MyString(const MyString& string)
{
_str = string._str;
cout << "浅拷贝" << endl;
}
*/
MyString(const MyString& string)
{
_str = new char[strlen(string._str) + 1];
strcpy(_str, string._str);
cout << "深拷贝" << endl;
}
~MyString()
{
delete _str;
}
private:
char* _str;
};
int main()
{
MyString string1;
string1._str = new char[10];
strcpy(string1._str, "abcdefghi");
// 拷贝
MyString string2 = string1;
// 打印string1._str字符串
cout << string1._str << endl;
// 打印string2._str字符串
cout << string2._str << endl;
// 打印string1._str的地址
cout << (void*)string1._str << endl;
// 打印string2._str的地址
cout << (void*)string2._str << endl;
return 0;
}
浅拷贝:
深拷贝:
12. 编译器对构造和拷贝构造的优化
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
: _a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
12.1 隐式类型转换,连续构造+拷贝构造->优化为直接构造
12.2 一个表达式中,连续构造+拷贝构造->优化为直接构造
12.3 一个表达式中,连续拷贝构造+拷贝构造->优化为一个拷贝构造
12.4 一个表达式中,连续拷贝构造+赋值重载->无法优化
对象返回总结:
- 接收返回值对象,尽量拷贝构造方式接收,不要赋值接收。
- 函数中返回对象时,尽量返回匿名对象。
函数传参总结:尽量使用const &传参。
以下代码共调用多少次拷贝构造函数?
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
int main()
{
Widget x;
Widget y = f(f(x));
return 0;
}
实例化类对象x时调用的是构造函数。
当编译器不做优化时,共调用9次拷贝构造函数:
- 实参x传递给形参u(函数参数类型为类类型对象)
- Widget v(u);(使用已经存在的对象创建新对象)
- Widget w = v;(使用已经存在的对象创建新对象)
- 用w拷贝构造一个临时对象(函数返回类型为类类型对象)
- 临时对象传递给形参u(函数参数类型为类类型对象)
- Widget v(u);(使用已经存在的对象创建新对象)
- Widget w = v;(使用已经存在的对象创建新对象)
- 用w拷贝构造一个临时对象(函数返回类型为类类型对象)
- 用临时对象拷贝构造y(使用已经存在的对象创建新对象)
优化:
- 4、5合二为一:用w直接拷贝构造u
- 8、9合二为一:用w直接拷贝构造y
所以优化后共调用7次拷贝构造函数。
13. 构造和析构的调用顺序
析构函数的调用顺序与构造函数相反,即:先构造的后析构,后构造的先析构。
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a = 1;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int b = 2;
};
int main()
{
A a;
B b;
return 0;
}
当类对象作为类成员时:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a = 1;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int b = 2;
A a; // B类中有A类对象
};
int main()
{
B b;
return 0;
}
先调用对象成员的构造,再调用本类构造。析构相反。
14. main函数执行之前和执行之后可能有哪些操作?
main函数执行之前:
- 初始化全局变量和静态变量。如果没有被显式初始化,则被默认初始化为0。
- 调用全局对象和静态对象的构造函数。
main函数执行之后:
- 调用全局对象和静态对象的析构函数。
- 调用终止函数(atexit函数用来注册终止函数)。调用顺序与其注册顺序相反。
15. =default和=delete
C++11中,我们可以通过在参数列表后面写上=default来要求编译器生成默认成员函数。=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。
而=delete定义删除的函数——我们虽然声明了它们,但不能以任何方式使用它们。=delete必须出现在函数第一次声明的时候。
我们只能对类的6个默认成员函数使用=default,但可以对任何函数使用=delete。
class Date
{
public:
Date() = default; // 使用编译器默认生成的构造函数
Date(const Date& d) = delete; // 阻止拷贝
Date& operator=(const Date& d) = delete; // 阻止赋值
~Date() = default; // 使用编译器默认生成的析构函数
private:
int _year;
int _month;
int _day;
};
16. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类。
16.1 友元函数
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。一般来说,最好在类定义开始或结束前的位置集中声明友元。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同。
16.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C时A的友元。
友元关系不能继承。
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
17. 内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl; // ok
cout << a.h << endl; // ok
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
18. 匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n)
{
// ...
return n;
}
};
int main()
{
A aa1; // ok
A aa2(); // err 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
A(); // ok 匿名对象 生命周期只有这一行,下一行就会自动调用析构函数
// 匿名对象在这样场景下就很好用
Solution().Sum_Solution(10);
return 0;
}
19. 日期类的实现
19.1 Date.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
// <<运算符重载
friend ostream& operator<<(ostream& out, const Date& d);
// >>运算符重载
friend istream& operator>>(istream& in, Date& d);
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) const;
// 构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 打印
void Print() const;
// ==运算符重载
bool operator==(const Date& d) const;
// !=运算符重载
bool operator!=(const Date& d) const;
// <运算符重载
bool operator<(const Date& d) const;
// <=运算符重载
bool operator<=(const Date& d) const;
// >运算符重载
bool operator>(const Date& d) const;
// >=运算符重载
bool operator>=(const Date& d) const;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 日期-天数
Date operator-(int day) const;
// 日期-日期
int operator-(const Date& d) const;
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
// <<运算符重载
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日";
return out;
}
// >>运算符重载
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
19.2 Date.cpp
#include"Date.h"
// 获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
// 构造函数
Date::Date(int year, int month, int day)
{
if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month)))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "日期非法" << endl;
}
}
// 打印
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// ==运算符重载
bool Date::operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
// !=运算符重载
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
// <运算符重载
bool Date::operator<(const Date& d) const
{
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
// <=运算符重载
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;
}
// >运算符重载
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
// >=运算符重载
bool Date::operator>=(const Date& d) const
{
return !(*this < d);
}
// 日期+=天数
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day) const
{
Date tmp(*this);
tmp += day;
return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
// 日期-日期
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// 后置++
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
20. 特殊类设计
20.1 请设计一个类,不能被拷贝
拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
// ...
};
原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了。
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
- C++11
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
// ...
};
20.2 请设计一个类,只能在堆上创建对象
实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
// C++98
// 1. 只声明,不实现。因为实现可能会很麻烦,而你本身不需要
// 2. 声明成私有
HeapOnly(const HeapOnly&);
// or
// C++11
HeapOnly(const HeapOnly&) = delete;
};
20.3 请设计一个类,只能在栈上创建对象
同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
// 禁掉operator new可以把下面用new调用拷贝构造申请对象给禁掉
// StackOnly obj = StackOnly::CreateObj();
// StackOnly* ptr3 = new StackOnly(obj);
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
StackOnly()
:_a(0)
{}
private:
int _a;
};
20.4 请设计一个类,不能被继承
- C++98
构造函数私有化,派生类中调不到基类的构造函数,则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
- C++11
final关键字,final修饰类,表示该类不能被继承。
class A final
{
// ...
};
20.5 请设计一个类,只能创建一个对象(单例模式)
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:
- 饿汉模式
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
// 饿汉模式
// 优点:简单
// 缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 构造函数私有
Singleton() {};
// C++98 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
// or
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton m_instance;
};
Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
- 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
// 懒汉
// 优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
// 缺点:复杂
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance()
{
// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
if (nullptr == m_pInstance)
{
m_mtx.lock();
if (nullptr == m_pInstance)
{
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
// 构造函数私有
Singleton() {};
// 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* m_pInstance; // 单例对象指针
static mutex m_mtx; // 互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;
int main()
{
thread t1([] {cout << &Singleton::GetInstance() << endl; });
thread t2([] {cout << &Singleton::GetInstance() << endl; });
t1.join();
t2.join();
cout << &Singleton::GetInstance() << endl;
cout << &Singleton::GetInstance() << endl;
return 0;
}