书接上文类与对象(上),我们学习类与对象的一些基础知识,接下来我们接着学习。
在C++中,当你定义一个类时,即使没有显式地声明某些成员函数,编译器也会为该类自动生成一些默认的成员函数。
⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求
第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
在 C++ 里,构造函数是一种特殊的成员函数,其主要功能是在创建对象时对对象进行初始化。
其特点如下:
一、核心特性
- 命名规则:构造函数的名称必须和类名一样。
- 无返回值:它没有返回类型,连void也不需要。
- 自动调用:在创建对象时会自动执行。
- 可重载:可以有多个不同参数列表的构造函数。
- 初始化列表:能够高效地初始化成员变量。
定义:无参数或所有参数均有默认值的构造函数。
生成规则: 若类中未声明任何构造函数,编译器自动生成隐式默认构造函数;若已声明其他构造函数(如带参构造),需手动定义默认构造函数。
class Point {
private:
int x, y;
public:
// 方式一:无参构造函数
Point() : x(0), y(0) {}
// 方式二:全默认参数构造函数(同时也是默认构造函数)
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
支持参数列表,用于显式初始化对象。
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {}
};
// 使用示例
Person p("Alice", 30);
class Resource {
private:
int* data;
public:
// 浅拷贝(默认行为)
Resource(const Resource& other) : data(other.data) {}
// 深拷贝(手动实现)
Resource(const Resource& other) {
data = new int(*other.data); // 复制资源
}
};
示例(日期类的构造函数)
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,6,30);//自动调用
d1.Print();
return 0;
}
输出结果:
2025-6-30
构造函数的功能就相当于初始化函数。构造函数通过其自动调用的特性,在提升代码的容错率和可维护性方面发挥了重要作用。它们确保了每个对象在创建时都能以预期的方式被初始化,减少了因未初始化或错误初始化而导致的错误。
在 C++ 中,初始化列表(Member Initializer List)是构造函数特有的语法,用于在对象创建时直接初始化成员变量。相较于在构造函数体内赋值,初始化列表具有更高的效率和更严格的语义约束。
初始化列表位于构造函数参数列表之后,使用冒号(:)分隔,多个初始化项用逗号(,)连接。
class Point {
private:
int x;
int y;
public:
// 使用初始化列表初始化成员变量
Point(int a, int b) : x(a), y(b) {}
};
成员变量的初始化顺序由类中声明的顺序决定,而非初始化列表中的书写顺序。
class Test {
private:
int a;
int b;
public:
// 危险:a先初始化(用未初始化的b),再初始化b
Test(int value) : b(value), a(b) {} // 未定义行为!
};
正确写法:按声明顺序初始化。
Test(int value) : a(value), b(a) {} // 正确:a先初始化
class Base {
public:
Base(int value) {} // 无默认构造函数
};
class Derived : public Base {
private:
const int constant;
int& reference;
std::string name; // 有默认构造函数,但推荐用初始化列表
public:
Derived(int x, int& ref)
: Base(x), // 调用基类构造函数
constant(x), // 初始化const成员
reference(ref) // 初始化引用成员
// name("default") // 可选:显式初始化name
{}
};
通常使用初始化列表,对于自定义类型成员变量,会先使用初始化列表初始化。
#include
using namespace std;
class A
{
public:
A(int a) : _a(a) // 使用初始化列表初始化成员变量
{
cout << "A(int a)" << endl;
}
private:
int _a;
};
class B
{
public:
B(int a, int b)
: _a(b), _b(a) // 先初始化 _a 然后初始化 _b
{
cout << "B(int a, int b)" << endl;
}
private:
A _a; // A 的构造函数会先于 B 的构造函数执行
int _b;
};
int main()
{
B b(2, 3);
return 0;
}
输出结果:
A(int a)
B(int a,int b)
初始化列表是 C++ 构造函数的重要特性,能确保成员变量高效、正确地初始化。使用时需注意:
在 C++ 中,初始化列表(Member Initializer List)和构造函数体赋值是两种初始化对象成员变量的方式,但它们在语义、效率和适用性上存在显著差异。以下从多个维度对比分析两者的区别及最佳实践:
特性 | 初始化列表 | 构造函数体赋值 |
语法位置 | 构造函数参数列表后,使用冒号(:)开头 | 构造函数体内 |
执行时机 | 在对象创建时直接初始化成员 | 成员先默认构造,再通过赋值修改值 |
适用场景 | 必须用于const成员、引用成员、无默认构造的类型 | 适用于所有可赋值的成员 |
效率 | 通常更高(减少一次默认构造) | 通常较低(需先默认构造再赋值 |
初始化顺序 | 由成员在类中声明的顺序决定,而非列表书写顺序 | 按语句顺序执行赋值 |
1. 初始化列表(高效)
class Example {
private:
std::string name;
public:
// 仅调用一次std::string的带参构造函数
Example(const char* str) : name(str) {}
};
2. 构造函数体赋值(低效)
class Example {
private:
std::string name;
public:
// 先默认构造name,再调用operator=
Example(const char* str) {
name = str; // 等价于:name = std::string(str);
}
};
执行步骤分解:
初始化列表:直接调用std::string(const char*)构造函数。
构造函数体赋值:
1. const成员 常量成员必须在初始化时赋值,后续无法修改。
class Test {
private:
const int value;
public:
Test(int x) : value(x) {} // 正确
// Test(int x) { value = x; } // 错误:无法对const赋值
};
2. 引用成员
引用必须在初始化时绑定到对象,后续无法重新绑定。
class Test {
private:
int& ref;
public:
Test(int& x) : ref(x) {} // 正确
// Test(int& x) { ref = x; } // 错误:引用未初始化
};
3. 无默认构造函数的成员
若成员类型没有默认构造函数,必须显式调用其带参构造函数。
class Member {
public:
Member(int value) {} // 无默认构造函数
};
class Test {
private:
Member member;
public:
Test(int x) : member(x) {} // 正确
// Test(int x) { member = Member(x); } // 错误:无法默认构造member
};
4. 基类的非默认构造
派生类必须显式调用基类的带参构造函数。
class Base {
public:
Base(int value) {} // 无默认构造函数
};
class Derived : public Base {
public:
Derived(int x) : Base(x) {} // 正确
// Derived(int x) { /* 无法隐式调用Base的构造函数 */ } // 错误
};
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。
1.命名规则
析构函数名与类名相同,但前面加波浪号(~),且无返回类型(包括void)。
2.无参数
析构函数不能有参数,因此无法重载。每个类只能有一个析构函数。
3.自动调用
对象生命周期结束时自动调用,无需手动调用。
4.逆序销毁
派生类的析构函数会自动调用基类的析构函数,顺序与构造相反。
class Date
{
public:
Date(int year = 2025, int month = 6, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//析构函数
~Date()
{
_year = _month = _day = 0;
}
private:
int _year;
int _month;
int _day;
};
析构函数相当于C语言中的销毁函数,确保了在对象销毁时,所有由该对象使用的资源都能得到妥善处理,从而避免资源泄露和其他潜在问题。
并且它是自动调用的,可以大大提高代码的容错率。
若未显式定义析构函数,编译器会生成隐式默认析构函数,
其行为:
class Date
{
public:
Date(int year = 2025, int month = 6, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//析构函数
/*~Date()
{
_year = _month = _day = 0;
}*/
// 编译器会自动生成一个析构函数
private:
int _year;
int _month;
int _day;
};
默认的析构函数对于内置类型(如int、float等)的成员变量不做任何处理,因为内置类型的生命周期是自动管理的,对于自定义类型调用其析构函数。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//默认生成
private:
A a;
int _year;
int _month;
int _day;
};
int main()
{
Date date(2025, 6, 30);
date.Print();
// 当 date 离开作用域时,其析构函数将被调用
return 0;
}
输出结果:
2025-6-30
~A()
拷贝构造函数是一种特殊的构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),用于创建一个对象作为另一个同类型对象的副本。
一、核心特性
命名规则
与类名相同,无返回类型,参数为本类对象的引用(通常为const引用)。
默认生成规则
若未显式定义,编译器会生成隐式拷贝构造函数,执行浅拷贝(逐成员复制)。
调用场景
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& other) {
// 复制other的数据到当前对象
}
};
例如:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数,用于创建当前对象的副本
// 它接受一个对同类型对象的常量引用作为参数,
// 并复制其成员变量
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 30);
// 使用拷贝构造函数创建d1的副本d2
Date d2(d1); //拷贝构造
// 使用拷贝初始化(也是拷贝构造的一种形式)创建d1的副本d3
Date d3 = d1; //拷贝构造
d1.Print();
d2.Print();
d3.Print();
return 0;
}
输出结果:
2025-6-30
2025-6-30
2025-6-30
(1)拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使⽤传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用
错误代码如下:
Date(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
(2)如果类没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会按照对象的内存布局逐字节地复制数据(即浅拷贝或值拷贝)。这意味着对于类中的基本数据类型成员和指针成员,它们的值(或地址)会被直接复制到新创建的对象中。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数,用于创建当前对象的副本
// 它接受一个对同类型对象的常量引用作为参数,
// 并复制其成员变量
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 6, 30);
// 使用拷贝构造函数创建d1的副本d2
Date d2(d1); //拷贝构造
// 使用拷贝初始化(也是拷贝构造的一种形式)创建d1的副本d3
Date d3 = d1; //拷贝构造
d1.Print();
d2.Print();
d3.Print();
return 0;
}
输出结果:
2025-6-30
2025-6-30
2025-6-30
(3)编译器默认生成的拷贝构造函数执行的是值拷贝(也称为浅拷贝),它会逐字节地复制对象的数据。在大多数情况下,这对于只包含基本数据类型(如整数、浮点数等)的对象来说是足够的。但是在某些场景会出错。
class A
{
public:
A() :
data(new int(10))
{
} // 构造函数中动态分配内存
~A()
{
delete data;
} // 析构函数中释放内存
// 注意:这里没有显式定义拷贝构造函数
void Print() const
{
std::cout << *data << std::endl;
}
private:
int* data; // 指向动态分配内存的指针
};
int main()
{
A a1; // 创建第一个对象,分配内存
A a2 = a1;
// 拷贝构造第二个对象,但这里使用的是默认拷贝构造函数
// 现在 a1 和 a2 共享同一块内存
a1.Print(); // 输出 10
a2.Print(); // 输出 10
// 当 main 函数结束时,a2 和 a1 的析构函数将被调用
// 由于它们共享同一块内存,这块内存将被释放两次,
// 导致未定义行为
return 0;
}
问题:多个对象共享同一资源,析构时可能导致重复释放。
在 C++ 中,运算符重载允许程序员重新定义内置运算符(如+、-、=等)对自定义类型的行为。这使得自定义类型的对象可以像内置类型一样自然地参与运算。
1.函数形式
运算符重载可以是类的成员函数或全局函数:
2.不可重载的运算符
.、::、?:、sizeof、typeid 等。
3. 重载规则
1. 算术运算符(如+、-)
class Vector {
private:
double x, y;
public:
// 成员函数形式
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
// 全局函数形式(需访问私有成员时声明为友元)
friend Vector operator-(const Vector& a, const Vector& b);
};
// 全局函数实现
Vector operator-(const Vector& a, const Vector& b) {
return Vector(a.x - b.x, a.y - b.y);
}
2. 赋值运算符(如=)
class String {
private:
char* data;
public:
// 拷贝赋值运算符(深拷贝)
String& operator=(const String& other) {
if (this != &other) {
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
// 移动赋值运算符(C++11+)
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
3. 下标运算符([])
class Array {
private:
int* data;
size_t size;
public:
// 可读可写版本
int& operator[](size_t index) {
return data[index];
}
// 只读版本(常量对象调用)
const int& operator[](size_t index) const {
return data[index];
}
};
4. 流插入 / 提取运算符(<<、>>)
必须作为全局函数重载:
class Point {
private:
int x, y;
public:
friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
};
// 流插入运算符(输出)
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}
// 流提取运算符(输入)
std::istream& operator>>(std::istream& is, Point& p) {
is >> p.x >> p.y;
return is;
}
5. 自增 / 自减运算符(++、--)
用空参数区分前置和后置版本:
class Counter {
private:
int value;
public:
// 前置++:返回引用
Counter& operator++() {
++value;
return *this;
}
// 后置++:返回值(用int参数占位)
Counter operator++(int) {
Counter temp = *this;
++(*this);
return temp;
}
};
三、成员函数 vs 全局函数
场景 | 成员函数 | 全局函数 |
左侧操作数必须是本类对象 | ✅ | ❌ |
需要修改当前对象 | ✅ | ❌(通常通过返回新对象实现) |
允许隐式类型转换 | ❌(左侧操作数已固定为本类) | ✅(所有参数均可参与转换) |
访问私有成员 | 直接访问 | 需要友元声明 |
在 C++ 中,取地址运算符(&)可以被重载,用于自定义对象的地址获取行为。这一特性相对少见,但在某些特殊场景(如智能指针、内存池管理)中非常有用。以下详细解析取地址运算符重载的特性、应用场景及注意事项:
取地址运算符有两种重载形式:
非 const 版本:类型* operator&()
const 版本:const类型* operator&() const
class MyClass {
private:
int data;
public:
// 非const对象的取地址运算符重载
MyClass* operator&() {
return this;
}
// const对象的取地址运算符重载
const MyClass* operator&() const {
return this;
}
};
例如:
class Date
{
public:
Date(int year, int month, int day) :
_year(year),
_month(month),
_day(day)
{
}
// Print函数被声明为const,表示它不会修改类的任何成员
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2025, 6, 30);
d.Print(); // 调用const成员函数
return 0;
}
.取地址运算符重载
我们可以对自定义类型使用运算符需要对其进行重载,自然也可以使用&运算符。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};