中部
C++ 类对象的初始化和析构顺序
类的初始化顺序
整体上来说,在C++中,类对象的初始化顺序遵循以下规则:
1. 基类初始化顺序
如果当前类继承自一个或多个基类,它们将按照声明顺序进行初始化,但是在有虚继承和一般继承存在的情况下,优先虚继承。
比如虚继承:class MyClass : public Base1, public virtual Base2,此时应当先调用 Base2 的构造函数,再调用 Base1 的构造函数。
2. 成员变量初始化顺序
类的成员变量按照它们在类定义中的声明顺序进行初始化(注意,成员变量的初始化顺序只与声明的顺序有关!!)。
3. 执行构造函数
在基类和成员变量初始化完成后,执行类的构造函数。
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Base1 {
public:
Base1() { std::cout << "Base1 constructor" << std::endl; }
~Base1() {
std::cout << "Base1 destructor" << std::endl;
}
};
class Base2 {
public:
Base2() { std::cout << "Base2 constructor" << std::endl; }
~Base2() {
std::cout << "Base2 destructor" << std::endl;
}
};
class Base3 {
public:
Base3() { std::cout << "Base3 constructor" << std::endl; }
~Base3() {
std::cout << "Base3 destructor" << std::endl;
}
};
class MyClass : public virtual Base3, public Base1, public virtual Base2 {
public:
MyClass() : num1(1), num2(2) {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
private:
int num1;
int num2;
// 这个是为了看成员变量的初始化顺序
Base base;
};
int main() {
MyClass obj;
return 0;
}
输出结果:
Base3 constructor // 虚继承排第一
Base2 constructor // 虚继承优先
Base1 constructor // 普通基类
Base constructor // 成员函数构造
MyClass constructor // 构造函数
关于虚继承实际用得比较少,感兴趣的可以阅读以下这篇文章: https://www.jianshu.com/p/ab96f88e5285
4. 类的析构顺序
记住一点即可,类的析构顺序和构造顺序完全相反
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Base1 {
public:
Base1() { std::cout << "Base1 constructor" << std::endl; }
~Base1() {
std::cout << "Base1 destructor" << std::endl;
}
};
class Base2 {
public:
Base2() { std::cout << "Base2 constructor" << std::endl; }
~Base2() {
std::cout << "Base2 destructor" << std::endl;
}
};
class Base3 {
public:
Base3() { std::cout << "Base3 constructor" << std::endl; }
~Base3() {
std::cout << "Base3 destructor" << std::endl;
}
};
class MyClass : public virtual Base3, public Base1, public virtual Base2 {
public:
MyClass() : num1(1), num2(2) {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
private:
int num1;
int num2;
Base base;
};
int main() {
MyClass obj;
return 0;
}
输出结果:
Base3 constructor
Base2 constructor
Base1 constructor
Base constructor
MyClass constructor
MyClass destructor
Base destructor
Base1 destructor
Base2 destructor
Base3 destructor
C++ 析构函数可以抛出异常吗?
首先,从语法层面 C++ 并没有禁止析构函数抛出异常,但是实践中建议不要这样做。
1. Effective C++ 条款08:别让异常逃离析构函数
由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。
另外,我们都知道在容器析构时,会逐个调用容器中的对象析构函数,而某个对象析构时抛出异常还会引起后续的对象无法被析构,导致资源泄漏。
这里的资源可以是内存,也可以是数据库连接,或者其他类型的计算机资源。
析构函数是由C++来调用的,源代码中不包含对它的调用,因此它抛出的异常不可被捕获。
对于栈中的对象而言,在它离开作用域时会被析构;对于堆中的对象而言,在它被delete时析构。
如:
class C{
public:
~C(){ throw 1;}
};
void main(){
try{
C c;
}
catch(...){}
}
析构的异常并不会被捕获,因为try代码块中只有一行代码C c,它并未抛出异常。运行时会产生这样的错误输出:
libC++abi.dylib: terminating with uncaught exception of type int
如果在析构函数中真的可能存在异常,该如何处理呢?
看一个例子,假设你使用一个 Class 负责数据库连接:
class DBConnection
{
public:
...
static DBConnection create(); //返回DBConnection对象;为求简化暂略参数
void close(); //关闭联机;失败则抛出异常。
};
为确保客户不忘记在DBConnection对象身上调用 close(),一个合理的想法是创建一个用来管理DBConection资源的class,并在其析构函数中调用close。
这就是著名的以对象管理资源即 RAII。
//这个class用来管理DBConnection对象
class DBConn
{
public:
...
DBConn(const DBConnection& db)
{
this->db=db;
}
~DBConn() //确保数据库连接总是会被关闭
{
db.close();
}
private:
DBConnection db;
};
如果调用close成功,没有任何问题。但如果该调用导致异常,DBConn析构函数会传播该异常,如果离开析构函数,那会造成问题,解决办法如下:
1.1. 如果close抛出异常就结束程序,通常调用abort完成:
DBConn::~DBconn()
{
try
{
db.close();
}
catch(...)
{
abort();
}
}
如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强制结束程序”是个合理选项,毕竟它可以阻止异常从析构函数传播出去导致不明确行为。
1.2. 吞下因调用 close 而发生的异常
DBConn::~DBConn
{
try{ db.close();}
catch(...)
{
//制作运转记录,记下对close的调用失败!
}
}
一般而言,将异常吞掉不太好,然而有时候吞下异常比“草率结束程序”或“不明确行为带来的风险”好。
能够这么做的一个前提就是程序必须能够继续可靠的执行。
2. 重新设计 DBConn 接口,使客户有机会对出现的异常作出反应
我们可以给DBConn添加一个close函数,赋予客户一个机会可以处理“因该操作而发生的异常”。
把调用close的责任从DBConn析构函数手上移到DBConn客户手中,你也许会认为它违反了“让接口容易被正确使用”的忠告。
实际上这污名并不成立。如果某个操作可能在失败的时候抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
3. 总结
析构函数可以抛出异常,但是这种做法是非常危险的,通常不推荐。 因为析构函数具有一种清理资源的特性,如果析构函数本身抛出异常,可能导致以下问题:
资源泄露:当一个对象被析构时,析构函数负责释放该对象持有的资源。如果析构函数抛出异常,这个过程可能会中断,导致资源泄露。
叠加异常:如果析构函数在处理另一个异常时抛出异常,会导致异常叠加。这种情况下,程序将无法处理两个异常,从而可能导致未定义行为或程序崩溃。
为了避免这些问题,通常建议在析构函数中处理异常或者避免执行会抛出异常的函数,可以在析构函数中使用 try-catch 块来捕获和处理潜在的异常,确保资源得到正确释放和清理。 保证程序的稳定性和健壮性。
参考:
https://blog.ycshao.com/2012/03/22/effective-c-item-8-prevent-exceptions-from-leaving-destructors/
https://blog.csdn.net/wallwind/article/details/6765167
https://blog.csdn.net/K346K346/article/details/55214384
https://harttle.land/2015/07/26/effective-cpp-8.html
C++ 中深拷贝和浅拷贝
C++中的深拷贝和浅拷贝涉及到对象的复制。
当对象包含指针成员时,这两种拷贝方式的区别变得尤为重要。
1. 浅拷贝(Shallow Copy)
浅拷贝是一种简单的拷贝方式,它仅复制对象的基本类型成员和指针成员的值,而不复制指针所指向的内存。
这可能导致两个对象共享相同的资源,从而引发潜在的问题,如内存泄漏、意外修改共享资源等。
一般来说编译器默认帮我们实现的拷贝构造函数就是一种浅拷贝。POD(plain old data) 类型的数据就适合浅拷贝,简单来说,浅拷贝也可以理解为直接按 bit 位复制,基本等价于 memcpy()函数。
2. 深拷贝(Deep Copy)
深拷贝不仅复制对象的基本类型成员和指针成员的值,还复制指针所指向的内存。
因此,两个对象不会共享相同的资源,避免了潜在问题。
深拷贝通常需要显式实现拷贝构造函数和赋值运算符重载。
#include <iostream>
#include <cstring>
class MyClass {
public:
MyClass(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 深拷贝的拷贝构造函数
MyClass(const MyClass& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 深拷贝的赋值运算符重载
MyClass& operator=(const MyClass& other) {
if (this == &other) {
return *this;
}
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
return *this;
}
void SetString(const char* str) {
if (data != NULL) {
delete[] data;
}
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~MyClass() {
delete[] data;
}
void print() {
std::cout << data << std::endl;
}
private:
char* data;
};
int main() {
MyClass obj1("Hello, World!");
MyClass obj2 = obj1; // 深拷贝
obj1.print(); // 输出:Hello, World!
obj2.print(); // 输出:Hello, World!
// 修改obj2中的数据,不会影响obj1
obj1.SetString("Test");
obj2.print(); // 输出:Hello, World!
return 0;
}
上面例子中,实现了一个简单的MyClass类,其中包含一个指向动态分配内存的指针。
在拷贝构造函数和赋值运算符重载中,重新动态分配了内存,实现了深拷贝。
这样,当创建一个新对象并从另一个对象拷贝数据时,两个对象不会共享相同的资源。
C++多态的实现方式
C++中的多态是指同一个函数或者操作在不同的对象上有不同的表现形式。
C++实现多态的方法主要包括虚函数、纯虚函数和模板函数
其中虚函数、纯虚函数实现的多态叫动态多态,模板函数、重载等实现的叫静态多态。
区分静态多态和动态多态的一个方法就是看决定所调用的具体方法是在编译期还是运行时,运行时就叫动态多态。
1. 虚函数、纯虚函数实现多态
在 C++ 中,可以使用虚函数来实现多态性。
虚函数是指在基类中声明的函数,它在派生类中可以被重写。
当我们使用基类指针或引用指向派生类对象时,通过虚函数的机制,可以调用到派生类中重写的函数,从而实现多态。
C++ 的多态必须满足两个条件:
必须通过基类的指针或者引用调用虚函数
被调用的函数是虚函数,且必须完成对基类虚函数的重写
class Shape {
public:
virtual int area() = 0;
};
class Rectangle: public Shape {
public:
int area () {
cout << "Rectangle class area :";
return (width * height);
}
};
class Triangle: public Shape{
public:
int area () {
cout << "Triangle class area :";
return (width * height / 2);
}
};
int main() {
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
shape = &rec;
shape->area();
shape = &tri;
shape->area();
return 0;
}
2. 模板函数多态
模板函数可以根据传递参数的不同类型,自动生成相应类型的函数代码。模板函数可以用来实现多态。
template <class T>
T GetMax (T a, T b) {
return (a>b?a:b);
}
int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=GetMax<int>(i,j);
n=GetMax<long>(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}
在上面这个例子用,编译器会生成两个 GetMax 函数实例,参数类型分别是 int 和 long 类型,这种调用的函数在编译期就能确定下来的叫静态多态。
3. 函数重载多态
静态多态还包括了函数重载。
函数重载(Function Overloading)是静态多态(Static Polymorphism)的一种表现形式。静态多态是指在编译时确定具体使用哪个函数或操作,而不是在运行时确定。这与动态多态(Dynamic Polymorphism)不同,后者通常通过虚函数(virtual functions)和继承机制在运行时实现。
函数重载允许在同一个作用域内定义多个同名函数,但这些函数的参数列表(参数的数量或类型)必须不同。编译器在编译时根据调用时提供的参数类型和数量来确定应该调用哪个重载函数。
#include <iostream>
#include <string>
// 重载函数,打印整数
void print(int i) {
std::cout << "Integer: " << i << std::endl;
}
// 重载函数,打印浮点数
void print(double d) {
std::cout << "Double: " << d << std::endl;
}
// 重载函数,打印字符串
void print(const std::string& str) {
std::cout << "String: " << str << std::endl;
}
int main() {
print(42); // 调用打印整数的函数
print(3.14); // 调用打印浮点数的函数
print("Hello"); // 调用打印字符串的函数
return 0;
}
在这个例子中,print 函数被重载了三次,分别用于打印整数、浮点数和字符串。编译器在编译时根据传递给 print 函数的参数类型来确定应该调用哪个版本的 print 函数。
需要注意的是,函数重载是静态绑定的,即在编译时就确定了调用哪个函数。这与虚函数(virtual functions)不同,后者通过动态绑定在运行时确定调用哪个函数。
this 指针
关于 this 指针
this 是一个指向当前对象的指针。
其实在面向对象的编程语言中,都存在this指针这种机制, Java、C++ 叫 this,而 Python 中叫 self。
在类的成员函数中访问类的成员变量或调用成员函数时,编译器会隐式地将当前对象的地址作为 this 指针传递给成员函数。
因此,this 指针可以用来访问类的成员变量和成员函数,以及在成员函数中引用当前对象。
#include <iostream>
class MyClass {
public:
MyClass(int value) : value_(value) {}
// 成员函数中使用 this 指针访问成员变量
void printValue() const {
std::cout << "Value: " << this->value_ << std::endl;
}
// 使用 this 指针实现链式调用
MyClass& setValue(int value) {
this->value_ = value;
return *this; // 返回当前对象的引用
}
private:
int value_;
};
int main() {
MyClass obj(10);
obj.printValue(); // 输出 "Value: 10"
// 链式调用 setValue 函数
obj.setValue(20).setValue(30);
obj.printValue(); // 输出 "Value: 30"
return 0;
}
在常量成员函数(const member function)中,this 指针的类型是指向常量对象的常量指针(const pointer to const object),因此不能用来修改成员变量的值。
1. static 函数不能访问成员变量
在C++中,static 函数是一种静态成员函数,它与类本身相关,而不是与类的对象相关。
大家可以将 static 函数视为在类作用域下的全局函数,而非成员函数。
因为静态函数没有 this 指针,所以它不能访问任何非静态成员变量。
如果在静态函数中尝试访问非静态成员变量,编译器会报错。
下面这个示例代码,说明了静态函数不能访问非静态成员变量:
class MyClass {
public:
static void myStaticFunction() {
// 以下代码会导致编译错误
// cout << this->myMemberVariable << endl;
}
private:
int myMemberVariable;
};
在上面的示例中,myStaticFunction是一个静态函数,尝试使用this指针来访问非静态成员变量myMemberVariable,但这会导致编译错误。
2. 总结:
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。
不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。
这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。