目录
合成析构函数(Synthesized Destructor)
带参数的构造函数(Parameterized Constructor)
作用域解析运算符(scope resolution operator)
为什么要进行对象的初始化和清理
对象的初始化和清理是确保程序安全性的重要环节。
对象没有正确初始化可能导致未定义的行为,包括访问未初始化的成员变量、调用未初始化的指针等。这些问题往往会导致程序崩溃、内存泄漏或者产生错误的计算结果。因此,在使用对象或变量之前,应该始终确保它们已经被正确地初始化。
另一方面,对象的清理也同样重要。如果一个对象占用了资源(如分配了内存、打开了文件等),在不再需要该对象时,及时清理这些资源非常重要。否则,就会造成资源泄漏,导致内存泄漏或文件句柄泄漏等问题,从而浪费系统资源并可能影响系统的稳定性和性能。
通过正确地初始化和清理对象,可以提高程序的安全性、稳定性和可维护性。
构造函数和析构函数
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
C++利用了构造函数和析构函数进行对象的初始化和清理 ,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们没有显式地提供构造函数和析构函数,编译器会提供默认的构造函数和析构函数,并进行空实现。这些默认的构造函数和析构函数被称为合成构造函数(Synthesized Constructors)和合成析构函数(Synthesized Destructors)。
构造函数(Constructor)
构造函数语法
类名(){}
-
没有返回值也不写void
-
是在创建对象时被调用的特殊成员函数,用于初始化对象的数据成员。
-
函数名称与类名相同
-
构造函数可以有参数,因此可以发生重载
-
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次(初始化操作)
调用时机
构造函数在对象创建时自动调用,它可以执行各种初始化操作,例如给对象的数据成员赋初值、分配动态内存、打开文件等。构造函数不需要手动调用,而是在以下情况下自动调用:
- 当创建一个对象时,使用
new
运算符创建动态对象时会调用构造函数。- 当定义一个局部对象时,在对象定义处会自动调用构造函数。
- 当一个对象作为函数参数进行传递时,函数的形参会调用拷贝构造函数来初始化。
构造函数的调用方式
括号法
通过在创建对象时使用圆括号,传递相应的参数来调用构造函数。
MyClass obj(arg1, arg2);
显式法
通过使用关键字explicit
显式调用构造函数。这种方式可以防止隐式类型转换。
MyClass obj = MyClass(arg1, arg2);
隐式转换法
当需要创建对象并且存在类型转换时,编译器可以自动进行隐式类型转换并调用合适的构造函数。
MyClass obj = arg;
其中arg
是不同类型的对象或基本数据类型,编译器会根据需要调用构造函数进行类型转换并创建新的对象。
构造函数分类
分类方式
按参数分为有参构造和无参构造
1.有参构造函数带有参数,用于接收外部传入的值来初始化对象的成员变量;
2.无参构造函数没有参数,可以使用默认值或者对成员变量进行默认初始化。
按类型分为普通构造和拷贝构造
1.普通构造函数用于创建新的对象,并根据传入的参数来初始化对象的成员变量;
2.拷贝构造函数用于使用一个已存在的对象来创建一个新对象,将已有对象的值复制给新对象的成员变量。
默认构造函数(Default Constructor)
定义
没有参数的构造函数被称为默认构造函数。当对象被创建时,如果没有提供任何参数,就会调用默认构造函数。默认构造函数可以用来初始化对象的数据成员为默认值。如果我们没有定义构造函数,编译器会提供一个空实现的默认构造函数。
编译器生成的默认构造函数和析构函数
默认构造函数(Default Constructor)
如果没有显式定义构造函数,编译器会自动生成一个空参数的默认构造函数。默认构造函数不执行任何操作,只是将对象的成员变量初始化为默认值(例如数值类型为0,指针类型为nullptr,类类型使用默认构造函数进行初始化)。
合成析构函数(Synthesized Destructor)
如果没有显式定义析构函数,编译器会自动生成一个析构函数。合成析构函数会按照成员变量声明的逆序,自动调用每个成员变量的析构函数,并在函数体中执行一些清理工作(例如释放内存、关闭文件等)。对于类类型的成员变量,会递归调用其析构函数。
注意
当我们显式定义了构造函数或析构函数时,编译器就不会再提供默认的构造函数或析构函数了。因此,在某些情况下,我们可能需要显式定义构造函数或析构函数,以满足特定的需求。
带参数的构造函数(Parameterized Constructor)
带有参数的构造函数可以接受一定数量和类型的参数,用来初始化对象的数据成员。通过传递参数给构造函数,我们可以在创建对象时将特定的值赋给对象的数据成员。
拷贝构造函数(Copy Constructor)
定义
拷贝构造函数用于创建一个新对象,并将现有对象的值复制到新对象中。它的参数是同一类的另一个对象的引用。拷贝构造函数通常用于对象的复制和初始化过程,例如通过赋值、函数传参或返回值返回对象时的复制操作。
被调用情况
使用一个已经创建完毕的对象来初始化一个新对象
当使用一个已存在的对象来创建一个新对象时,拷贝构造函数会被调用。这包括通过直接赋值、作为函数参数传递、或者作为函数返回值返回等方式。
值传递的方式给函数参数传值
当一个对象作为函数参数以值传递的方式传递时,编译器会调用拷贝构造函数来创建函数内部的副本。
以值方式返回局部对象
当函数返回一个对象的副本时,拷贝构造函数会被调用。这通常发生在函数返回一个局部对象的情况下。
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
析构函数(Destructor)
定义
析构函数是在对象被销毁时自动调用的特殊成员函数。它的主要作用是在对象生命周期结束时执行清理工作,例如释放动态分配的内存、关闭文件或释放其他资源。
语法
~类名(){}
-
析构函数,没有返回值也不写void
-
函数名称与类名相同,在名称前加上符号 ~
-
析构函数不可以有参数,因此不可以发生重载
-
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
被自动调用情况
当对象的作用域结束时,例如当一个对象是局部变量,它的作用域结束后会自动调用析构函数。
在使用delete关键字手动释放通过new操作符分配的内存时,会调用对象的析构函数。
在容器(如数组、容器类)中的对象被删除时(如调用erase()),会自动调用对象的析构函数。
注意
当对象是通过new创建的,并存储在堆上时,必须手动调用delete运算符来显式调用析构函数,以避免内存泄漏。
示例
#include <iostream>
using namespace std;
class Person {
public:
// 无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
// 有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数,年龄为" << age << "岁!" << endl;
}
// 拷贝构造函数
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数,复制了一个" << age << "岁的对象!" << endl;
}
// 析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};
// 调用无参构造函数
void test01() {
Person p; // 调用无参构造函数
}
// 调用有参的构造函数和拷贝构造函数
void test02() {
// 括号法
Person p1(20);
// 显式法
Person p2 = Person(30);
Person p3 = Person(p2);
// 隐式转换法
Person p4 = 40;
Person p5 = p4;
Person p6(p4);
}
int main() {
test01();
test02();
return 0;
}
这段代码实现了一个简单的示例,展示了构造函数的使用和调用方式。下面是对代码的解释:
定义了一个名为
Person
的类,在类中声明了一个公有成员变量age
和三个构造函数:无参构造函数、有参构造函数和拷贝构造函数。在无参构造函数中,输出 "无参构造函数!" 的信息。
在有参构造函数中,根据传入的参数初始化
age
成员变量,并输出 "有参构造函数,年龄为X岁!" 的信息(其中 X 是传入的参数值)。在拷贝构造函数中,通过将传入对象的
age
成员变量赋值给当前对象的age
成员变量,创建了一个新的对象,并输出 "拷贝构造函数,复制了一个X岁的对象!" 的信息(其中 X 是传入对象的age
值)。定义了
test01
函数,它用于测试无参构造函数的调用。在该函数中,创建一个Person
对象p
,这将调用无参构造函数并输出相应信息。定义了
test02
函数,它用于测试有参构造函数和拷贝构造函数的不同调用方式。
- 对于括号法,使用
Person p1(20)
创建一个带有整数参数的Person
对象,调用有参构造函数,并输出相应信息。- 对于显式法,使用
Person p2 = Person(30)
显式地调用有参构造函数创建对象p2
,同时在创建过程中输出相应信息。同样地,使用Person p3 = Person(p2)
显式地调用拷贝构造函数创建对象p3
,并输出相应信息。- 对于隐式转换法,使用
Person p4 = 40
隐式地调用有参构造函数创建对象p4
,并输出相应信息。然后使用Person p5 = p4
隐式地调用拷贝构造函数创建对象p5
,并输出相应信息。此外,还可以使用拷贝构造函数直接将一个已存在的对象传递给构造函数来创建新的对象,例如Person p6(p4)
。主函数
main
中调用了test01
和test02
函数,使得这些函数被执行并观察输出结果。
初始化列表(initializer list)
定义
初始化列表是一种在 C++ 中用于初始化类成员变量的语法。通过初始化列表,可以在创建对象时直接为成员变量赋初值。
语法
构造函数():属性1(值1),属性2(值2)... {}
具体来说,初始化列表使用冒号(:)紧跟在构造函数的声明后面,并在列表中指定成员变量的初始值。构造函数体内部可以进行其他操作,但初始化列表在构造函数体执行之前完成。
示例
class MyClass {
private:
int number;
double value;
public:
// 构造函数,使用初始化列表初始化成员变量
MyClass(int n, double v) : number(n), value(v) {
// 构造函数体
// 可以在这里进行其他操作
}
// ...
};
在上面的示例中,
MyClass
类有两个私有成员变量number
和value
。构造函数使用初始化列表来分别初始化这两个成员变量。在构造函数的参数列表后面的冒号后,number(n)
表示将参数n
的值赋给成员变量number
,value(v)
表示将参数v
的值赋给成员变量value
。在构造函数的主体部分中,还可以进行其他操作。可以用无参构造函数,但是n,v 必须是具体的
MyClass() : number(1), value(1.0){}
优点
- 提供了一种更直观、简洁的方法来初始化成员变量。
- 可以避免成员变量的默认初始化,直接使用指定的初值。
- 对于常量成员变量或引用类型的成员变量,必须使用初始化列表进行初始化,因为它们只能在创建对象时初始化一次。
深拷贝与浅拷贝
浅拷贝(Shallow Copy)
简单的赋值拷贝操作
当进行浅拷贝时,仅拷贝对象或数据的地址引用,而不会创建新的内存空间。这意味着原对象和拷贝对象将共享同一块内存空间。如果其中一个对象修改了这块内存中的数据,另一个对象也会受到影响。
深拷贝(Deep Copy)
在堆区重新申请空间,进行拷贝操作
当进行深拷贝时,会创建一个新的内存空间,并将原对象的所有内容复制到新的内存空间中。这样,拷贝对象将有自己独立的内存空间,对其进行修改不会影响到原对象
分析
浅拷贝只复制了对象的引用,而没有复制对象的内容。因此,原对象和拷贝对象指向同一块内存空间。这种情况下,如果修改了拷贝对象的内容,原对象也会受到影响。常见的浅拷贝方式包括默认拷贝构造函数、赋值运算符等。
而深拷贝则完全复制了对象的内容,包括对象所引用的其他对象。这样,在拷贝对象中修改数据不会影响到原始对象,因为它们拥有各自独立的内存空间。要实现深拷贝,通常需要自定义拷贝构造函数或赋值运算符,确保深层次的数据也被复制到新的内存空间中。
注意
在某些情况下,特别是当对象包含动态分配的内存(例如指针、数组等)时,使用浅拷贝可能会导致问题。因为多个对象共享相同的内存,释放内存的时候可能会导致重复释放或悬空指针等问题。这时,深拷贝是一种更安全可靠的选择。
在执行深拷贝时,如果对象涉及到其他对象的引用,还需要对这些引用的对象进行递归深拷贝,以确保所有相关数据都被正确地复制。
在 C++ 中,默认情况下,类的拷贝构造函数和赋值运算符是浅拷贝的。如果需要进行深拷贝,可以自定义拷贝构造函数和赋值运算符,或者使用智能指针等工具来管理内存,确保对象复制的正确性和安全性。
类对象作为类成员
在 C++ 中,一个类可以作为另一个类的成员变量。这种情况下,我们称该成员为 对象成员
示例
class Address {
private:
std::string street;
std::string city;
public:
Address(const std::string& s, const std::string& c) : street(s), city(c) {}
// ...
};
class Person {
private:
std::string name;
int age;
Address address; // Address 类对象作为 Person 类的成员
public:
Person(const std::string& n, int a, const Address& addr) : name(n), age(a), address(addr) {}
// ...
};
在上面的示例中,有两个类:
Address
和Person
。Address
类表示地址,包含街道和城市两个成员变量。Person
类表示人员信息,包含姓名、年龄和一个Address
类对象作为成员变量。在
Person
类的构造函数中,使用初始化列表初始化name
、age
和address
成员变量。对于address
成员变量,传入一个Address
类对象作为参数,用于初始化它。通过这种方式,每个Person
对象都包含一个独立的Address
对象,作为其一部分。
构造和析构的顺序
构造函数的执行顺序是按照成员变量在类中声明的顺序进行的
先构造基类的成员变量,然后构造派生类的成员变量。如果一个类作为另一个类的成员对象,那么它将在包含它的类的构造函数中构造。
析构函数的执行顺序与构造函数相反
也就是说,先调用派生类的析构函数,然后调用基类的析构函数。同时,在一个类的析构函数中,成员变量的析构顺序与构造函数中的初始化顺序相反。
举例
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
~Base() { std::cout << "Base destructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor" << std::endl; }
~Derived() { std::cout << "Derived destructor" << std::endl; }
};
class MyClass {
private:
Base base;
Derived derived;
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
Base constructor
Derived constructor
MyClass constructor
MyClass destructor
Derived destructor
Base destructor
可以看到,构造函数的调用顺序是先构造
Base
的成员变量,然后是Derived
的成员变量,最后是MyClass
的构造函数。而析构函数的调用顺序则相反,先调用MyClass
的析构函数,然后是Derived
的析构函数,最后是Base
的析构函数。
总结
先调用对象成员的构造,再调用本类构造,析构函数相反
静态成员(Static Members)
在C++中,静态成员是属于类本身的,而不是类的实例。它们与类相关联,而不是与类的对象相关联。静态成员在所有类对象之间共享,只有一个副本存在于内存中。
静态成员可以包括静态变量和静态函数两种形式
静态变量(Static Variables)
定义
静态变量是在类声明中用关键字
static
声明的成员变量。它们在所有类对象之间共享相同的存储空间,并且可以在没有任何类对象实例的情况下访问。静态变量可以用于跨对象共享数据或用作计数器等。
特点
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
示例
class MyClass {
public:
static int count; // 静态变量声明
};
int MyClass::count = 0; // 静态变量的定义和初始化
int main() {
MyClass::count++; // 访问和修改静态变量
return 0;
}
静态函数(Static Functions)
定义
静态函数是在类声明中用关键字
static
声明的成员函数。它们不依赖于类的任何实例,可以直接通过类名访问。静态函数不能访问非静态成员变量,也不能调用非静态成员函数,因为它们没有隐式的this
指针。
特点
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
示例
class MathUtils {
public:
static int add(int a, int b) { // 静态函数声明
return a + b;
}
};
int result = MathUtils::add(3, 4); // 调用静态函数
静态成员变量访问方式
1.通过实例化一个对象,通过对象访问静态成员
Person p1;
p1.m_A = 100;
2.通过类名
格式: 类型 类::静态成员
int Person::m_A = 10;
作用域解析运算符(scope resolution operator)
在C++中,
::
运算符被称为作用域解析运算符(scope resolution operator)。它的主要作用是访问类、命名空间、结构体等作用域内的成员。具体来说,在类成员函数或类外部定义成员函数时可以使用::
运算符来指定要定义的函数属于哪个类的成员函数。总之,::
运算符允许我们在特定的作用域中访问成员,并指定所操作的对象或类。
ps:整理了足足一个上午,累麻了