C++———类与对象(中)

引言

书接上文类与对象(上),我们学习类与对象的一些基础知识,接下来我们接着学习。

 类的默认成员函数

在C++中,当你定义一个类时,即使没有显式地声明某些成员函数,编译器也会为该类自动生成一些默认的成员函数。

⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求

第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

C++———类与对象(中)_第1张图片 

构造函数 

在 C++ 里,构造函数是一种特殊的成员函数,其主要功能是在创建对象时对对象进行初始化。 

其特点如下: 

 一、核心特性

  1. 命名规则:构造函数的名称必须和类名一样。
  2. 无返回值:它没有返回类型,连void也不需要。
  3. 自动调用:在创建对象时会自动执行。
  4. 可重载:可以有多个不同参数列表的构造函数。
  5. 初始化列表:能够高效地初始化成员变量。

二、构造函数的分类

1. 默认构造函数

定义:无参数或所有参数均有默认值的构造函数。

生成规则: 若类中未声明任何构造函数,编译器自动生成隐式默认构造函数;若已声明其他构造函数(如带参构造),需手动定义默认构造函数。 

class Point {
private:
    int x, y;
public:
    // 方式一:无参构造函数
    Point() : x(0), y(0) {}

    // 方式二:全默认参数构造函数(同时也是默认构造函数)
    Point(int x = 0, int y = 0) : x(x), y(y) {}
};
 2. 带参数的构造函数

支持参数列表,用于显式初始化对象。

class Person {
private:
    std::string name;
    int age;
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
};

// 使用示例
Person p("Alice", 30);
3. 拷贝构造函数
  • 作用:使用已存在的对象初始化新对象。
  • 默认生成规则: 若未显式定义,编译器生成隐式拷贝构造函数(浅拷贝)。
  • 深拷贝与浅拷贝: 当类包含动态资源(如指针)时,需显式定义深拷贝构造函数。 
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) {}
};

二、注意事项

(1)初始化顺序规则

成员变量的初始化顺序由类中声明的顺序决定,而非初始化列表中的书写顺序。 

 

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先初始化

 

(2)每个成员变量只能初始化一次 

 

(3)必须使用初始化列表的场景

  • const成员:常量必须在初始化时赋值,后续无法修改。
  • 引用成员:引用必须在初始化时绑定到对象,后续无法重新绑定。
  • 无默认构造函数的成员:若成员类型没有默认构造函数,必须显式调用其带参构造函数。
  • 基类的非默认构造:派生类必须显式调用基类的带参构造函数。 
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
    {}
};

(4)尽量使用初始化列表初始化 

通常使用初始化列表,对于自定义类型成员变量,会先使用初始化列表初始化。 

#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++ 构造函数的重要特性,能确保成员变量高效、正确地初始化。使用时需注意:

  1. 优先使用初始化列表,而非构造函数体赋值。
  2. 遵循声明顺序进行初始化,避免依赖未初始化的变量。
  3. 处理特殊成员(const、引用、无默认构造的类型)。
  4. 结合默认成员初始化值简化代码。 

 

初始化列表 vs 构造函数体赋值 

在 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. 调用std::string()默认构造函数。
  2. 调用std::string::operator=(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的构造函数 */ } // 错误
};

 

析构函数 

 1.析构函数的概念

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,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()

 

拷贝构造函数 

1.拷贝构造函数的概念

拷贝构造函数是一种特殊的构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用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.函数形式

运算符重载可以是类的成员函数或全局函数:

  • 成员函数:返回类型 operator运算符(参数列表)
  • 全局函数:返回类型 operator运算符(参数1, 参数2)

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; 
};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(C++,c++,开发语言,学习,学习方法)