C++ 类与对象(全)

发布于:2025-07-31 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

一、类与对象

1、C++类与对象的关系

1.1类的定义

1.2对象的实例化

1.3类与对象的关系

1.4关键特性

1.5内存分配

1.6使用场景

2、对象指针

2.1 指向对象的指针

2.2 指向对象成员的指针

①指向对象数据成员的指针

②指向对象成员函数的指针

2.3 指向当前对象的this指针

this 指针的特点

this 指针的用途

this 指针的注意事项

3、对象数组

4、公用数据的保护

4.1 定义常对象

4.2 定义常对象成员

4.2.1 常数据成员

4.2.2 常成员函数

4.2.3 指向对象的常指针

4.2.4 指向常对象的指针变量

4.2.5 对象的常引用

4.2.6 const型数据的小结

4.2.7 mutable关键字

5、对象的动态建立和释放

5.1动态建立对象

5.1.1使用new操作符创建单个对象

5.1.2使用new操作符创建对象数组

5.2动态释放对象

5.2.1. 使用delete操作符释放单个对象

5.2.2. 使用delete[]操作符释放对象数组

5.3智能指针与动态内存管理(C++11 及以后)

6、对象的赋值和复制

6.1 对象的赋值(Assignment)

6.1.1 默认赋值行为

6.1.2 自定义赋值运算符

6.1.3 赋值运算符的注意事项

6.1.4说明

6.2 对象的复制(Copy)

6.2.1 默认拷贝构造函数

6.2.2 自定义拷贝构造函数

6.2.3 拷贝构造函数的触发场景

6.3 赋值与复制的核心区别

6.4深拷贝 vs 浅拷贝

6.5 移动语义(C++11+)

6.6 总结与最佳实践

7、不同对象间实现数据共享

7.1 把数据成员定义为静态

7.2 用静态数据成员函数访问静态数据成员

1. 静态数据成员

2. 静态成员函数

3. 静态成员函数访问静态数据成员的实例

4. 关键要点

8、允许访问私有数据的”朋友“

8.1 友元

核心概念

关键特性

典型应用场景

注意事项

8.1.1 可以访问私有数据的友元函数

核心概念

8.1.2 可以访问私有数据的友元类

8.1.3 全局函数作友元

核心概念

典型应用场景

使用注意事项

示例代码

总结

9、类模板

9.1 函数模板

9.2 类模板


一、类与对象

1、C++类与对象的关系

在C++中,类(Class)对象(Object)是面向对象编程(OOP)的核心概念。类是一种用户自定义的数据类型,用于描述对象的属性和行为;对象是类的具体实例,通过实例化类创建。

1.1类的定义

类是一个蓝图或模板,用于定义对象的属性和方法。属性通常以成员变量表示,行为以成员函数表示。类的定义包括访问修饰符(如publicprivateprotected),用于控制成员的访问权限。

class Person {
private:
    string name; // 私有成员变量
    int age;
​
public:
    void setName(string n) { name = n; } // 公有成员函数
    string getName() { return name; }
};

1.2对象的实例化

对象是类的具体实例,通过声明类变量创建。每个对象拥有独立的成员变量存储空间,但共享类的成员函数。

Person p1; // 创建Person类的对象p1
p1.setName("Alice"); // 调用成员函数
cout << p1.getName(); // 输出: Alice

1.3类与对象的关系

是抽象的概念,描述一类事物的共性;对象是具体的实体,具有类定义的属性和行为。例如:

  • 类"汽车"可能包含属性(颜色、型号)和方法(启动、刹车)。

  • 对象"我的红色丰田"是"汽车"类的一个实例。

1.4关键特性

  1. 封装:通过访问修饰符隐藏内部实现,暴露必要接口。

  2. 继承:允许派生类继承基类的成员,实现代码复用。

  3. 多态:通过虚函数实现同一接口的不同行为。

1.5内存分配

  • 类的成员函数存储在代码区,所有对象共享。

  • 每个对象的成员变量存储在栈或堆区,独立占用内存。

Person* p2 = new Person(); // 动态创建对象
delete p2; // 释放内存

1.6使用场景

类用于建模复杂系统,对象代表系统中的具体元素。例如:

  • 图形编辑器:类"Shape"派生"Circle"、"Rectangle"等子类。

  • 游戏开发:类"Character"实例化为玩家、敌人等对象。

2、对象指针

指针不仅可以在指向普通变量,还可以指向对象。

2.1 指向对象的指针

在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其数据成员的值。一个对象存储空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的地址,这就是指向对象的指针变量。

如果有一个类:

class Time
{public:
int hour;
int minute;
int sec;
​
void get_time();
};
void Time::get_time()
{
    cout<<hour<<":"<<minute<<":"<<sec<<endl;
}

在主函数中有:

Time *pt;
Time t1;
pt=&t1;

这样,pt就是指向Time类对象的指针变量,它指向对象t1.

定义指向类对象的指针变量的一般形式为:

类名 *对向指针名;

在以上基础上,可以通过对象指针pt来访问对象和对象共有成员

*pt                         //pt所指向的对象,即t1
​
(*pt).hour                  //pt所指向的对象中hour成员,即t1.hour
pt->hour                    //pt所指向的对象中hour成员,即t1.hour
​
(*pt).get_time()            //调用pt所指向的对象中的get_time函数,即t1.get_time
pt->get_time()              //调用pt所指向的对象中的get_time函数,即t1.get_time

示例

#include <iostream>
using namespace std;
​
class Time {
public:
    int hour;
    int minute;
    int sec;
​
    void get_time();
    void set_time(int h, int m, int s);
};
​
void Time::get_time() {
    cout << hour << ":" << minute << ":" << sec << endl;
}
​
void Time::set_time(int h, int m, int s) {
    hour = h;
    minute = m;
    sec = s;
}
​
int main() {
    // 创建Time对象
    Time t1;
    // 创建对象指针
    Time *ptr;
​
    // 指针指向对象
    ptr = &t1;
​
    // 通过指针访问成员函数
    ptr->set_time(10, 20, 30);
    
    // 通过指针访问数据成员并修改
    ptr->hour = 11;
    ptr->minute = 22;
    ptr->sec = 33;
​
    // 通过指针调用成员函数
    ptr->get_time();  // 输出: 11:22:33
​
    // 使用new动态分配对象
    ptr = new Time;
    ptr->set_time(23, 59, 59);
    ptr->get_time();  // 输出: 23:59:59
​
    // 释放动态分配的内存
    delete ptr;
​
    return 0;
}

2.2 指向对象成员的指针

对象有地址,存放对象的起始地址的指针变量就是指向对象的指针变量对象中的成员也是有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量

①指向对象数据成员的指针

定义形式:数据类型名 *指针变量名

例:如果Time类的数据成员hour为公用的整形数据,则可以在类外通过指向对象数据成员的指针变量访问对像数据成员hour:

p1=&t1.hour;        //将对象t1的数据成员hour的地址赋给p1,使p1指向t1.hour
cout<<*p1<<endl;    //输出t1.hour的值
②指向对象成员函数的指针

定义指向对象成员函数的指针变量的方法和定义指向普通函数的指针变量方法有所不同。

普通函数的指针变量定义方法:类型名(*指针变量名)(参数列表);

例:void(*p)(); //p是指向void型函数的指针变量

//可以使p指向一个函数,并通过指针变量调用函数

p=fun; //将fun函数的入口地址赋给指针变量p,p就指向了函数fun

(*p)(); //调用fun函数

成员函数与普通函数的根本区别:成员函数是类中的一个成员

编译系统在运行指针赋值语句时,指着变量的类型必须与右值函数的类型相匹配,要求在以下三方面都要匹配:

①函数参数的类型和参数个数

②函数返回值的类型

③所属的类

指针变量p与类无关,但成员函数却属于其类。因此以上三点中不符合③。因此,要区别普通函数和成员函数的不同性质,不能在类外直接用成员函数名作为函数入口地址去调用成员函数。

定义指向成员函数的指针变量:数据类型名 (类名::指针变量名) (参数列表);

示例:

void (Time::*p2)(); //定义p2为指向Time类中共用成员函数的指针变量

注意: (Time::p2)两侧的括号是不可以省略的,因为( )的优先级高于指针符的。如果没有,就是void Time::p2(),相当于void (Time::*p2())就成了返回值为void型指针的函数

可以让它指向一个公用成员函数,只需把公用成员函数的入口地址赋给一个指向公用成员函数的指针变量即可

p2 = &Time::get_time;

使指针变量指向一个公用函数的一般形式为:指针变量名 = &类名::成员函数名;

示例:用对象指针的方式输出时、分、秒

#include<iostream>
using namespace std;
​
class Time
{
    public:
    Time(int,int,int);          //声明构造函数
    int hour;
    int minute;
    int sec;
    void get_time();
};  
​
Time::Time(int h,int m,int s)   //定义构造函数
{
    hour=h;
    minute=m;
    sec=s;
}
​
void Time::get_time()
{
    cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
​
int main()
{
    Time t1(10,13,56);      //定义Time类对象t1并初始化
    int* p1=&t1.hour;       //定义指向整型数据的指针变量p1,并使p1指向t1.hour
    cout<<*p1<<endl;        //输出p1所指的数据成员t1.hour
    t1.get_time();          //调用对象t1的公用成员函数get_time
    
    Time *p2=&t1;           //定义指向Time类对象的指针变量p2,并使p2指向t1
    p2->get_time();         //调用p2所指向对象(t1)的get_time函数
    
    void (Time::*p3)();     //定义指向Time类公用成员函数的指针变量p3
    p3=&Time::get_time;     //使p3指向Time类公用函数get_time
    //成员函数的入口地址的正确写法是  &类名::成员函数名
    (t1.*p3)();             //调用对象t1中p3所指向的成员函数(即t1.get_time())
    
    return 0;
}

2.3 指向当前对象的this指针

每个对象中的数据成员都分别占有存储空间,如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同的对象都调用同一个函数的目标代码。

每个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用成员函数所在的对象的起始地址。

this 指针的特点
  1. 隐含参数:每个非静态成员函数(包括构造函数和析构函数)都有一个隐含的 this 指针参数。

  2. 类型:对于类 Timethis 的类型是 Time* const(即指向常量对象的指针,不能修改指针本身)。

  3. 作用域:仅在成员函数内部可见。

this 指针的用途

区分同名的成员变量和参数

当参数名与成员变量名相同时,可以使用 this 指针明确引用成员变量。

示例:

class Time {
public:
    int hour;
    Time(int hour) {
        this->hour = hour;  // this->hour 指向成员变量,hour 是参数
    }
};

返回当前对象的引用

成员函数可以返回 *this,用于实现链式调用(如 obj.func1().func2())。

示例:

class Time {
public:
    int hour;
    Time& setHour(int h) {
        hour = h;
        return *this;  // 返回当前对象的引用
    }
};
​
// 使用链式调用
Time t;
t.setHour(10).setHour(12);  // 连续设置小时

在成员函数中获取当前对象的地址

返回当前对象的指针,用于需要对象指针的场景。

class Time {
public:
    Time* getPtr() {
        return this;  // 返回当前对象的地址
    }
};
​
// 使用
Time t;
Time* ptr = t.getPtr();  // ptr 指向 t

在析构函数中避免资源重复释放

通过 this 指针比较对象身份,防止重复释放资源。

示例:

class Resource {
private:
    bool released = false;
public:
    ~Resource() {
        if (!released) {
            released = true;
            // 释放资源的代码
        }
    }
};
this 指针的注意事项
  1. 静态成员函数没有 this 指针:静态成员函数属于类而非对象,因此没有 this 指针。

  2. this 不可修改this 是一个常量指针,不能被赋值(如 this = nullptr; 是错误的)。

  3. 在构造函数和析构函数中使用this 在构造函数中指向正在创建的对象,在析构函数中指向正在销毁的对象。

  4. 空指针调用成员函数:若通过空指针调用成员函数,且函数中使用了 this,会导致运行时错误(如 nullptr->func())。

示例:使用 this 指针的完整代码

#include <iostream>
using namespace std;
​
class Time {
private:
    int hour;
    int minute;
    int sec;
​
public:
    Time(int hour, int minute, int sec) {
        this->hour = hour;    // 使用 this 区分参数和成员变量
        this->minute = minute;
        this->sec = sec;
    }
​
    Time& addHours(int h) {
        this->hour += h;      // 使用 this 明确访问成员变量
        return *this;         // 返回当前对象的引用,支持链式调用
    }
​
    void printTime() const {  // const 成员函数中的 this 是 const Time*
        cout << this->hour << ":" << minute << ":" << sec << endl;
    }
};
​
int main() {
    Time t(10, 20, 30);
    t.addHours(2).printTime();  // 链式调用:12:20:30
    return 0;
}

总结

this 指针是 C++ 实现面向对象编程的基础机制之一,它提供了成员函数访问和操作当前对象的能力。合理使用 this 指针可以提高代码的可读性和灵活性,特别是在处理参数名冲突和实现链式调用时。

3、对象数组

在 C++ 里,对象数组指的是存储同一类对象的数组。它将多个对象聚集在一起,借助统一的数组名和下标来对这些对象进行访问。下面为你介绍 C++ 对象数组的核心要点:

3.1. 对象数组的定义与初始化

可以采用以下方式定义对象数组:

ClassName arrayName[size]; // 声明包含size个ClassName对象的数组

初始化对象数组有多种办法:

  • 默认构造函数初始化

    :要是类存在默认构造函数,那么数组里的所有对象都会通过该默认构造函数来初始化。

    MyClass arr[3]; // 调用MyClass的默认构造函数3次
  • 带参数的构造函数初始化

    :可以逐个初始化数组元素。

    MyClass arr[2] = {MyClass(1), MyClass(2)}; // 假设有MyClass(int)构造函数
  • 聚合初始化

    (适用于有默认成员初始化器的类):

    class Point {
    public:
        int x = 0;
        int y = 0;
    };
    ​
    Point points[2] = {{1, 2}, {3, 4}}; // 直接初始化成员

3.2. 对象数组的访问与使用

和普通数组一样,对象数组也是通过下标来访问元素的。

arr[0].method(); // 调用第一个对象的方法
arr[1].member = value; // 访问或修改成员变量

3.3. 对象数组的内存管理

  • 栈内存:若在函数内部定义对象数组,它会被存储在栈上,函数执行结束后会自动释放内存。

    void func() {
        MyClass arr[10]; // 栈上分配10个对象的空间
    } // 函数结束后,数组内存自动释放
  • 堆内存

    :借助动态内存分配(new)创建的对象数组,需要手动用delete[]释放内存。

    MyClass* arr = new MyClass[5]; // 堆上分配内存
    delete[] arr; // 释放内存,防止内存泄漏

示例代码

下面通过一个完整的例子来展示对象数组的使用:

#include <iostream>
using namespace std;
​
class Rectangle {
private:
    int width, height;
public:
    // 默认构造函数
    Rectangle() : width(0), height(0) {}
    
    // 带参数的构造函数
    Rectangle(int w, int h) : width(w), height(h) {}
    
    // 计算面积的方法
    int area() const {
        return width * height;
    }
};
​
int main() {
    // 使用默认构造函数创建对象数组
    Rectangle rects1[2];
    
    // 使用带参数的构造函数初始化对象数组
    Rectangle rects2[2] = {Rectangle(3, 4), Rectangle(5, 6)};
    
    // 动态分配对象数组
    Rectangle* rects3 = new Rectangle[2]{Rectangle(1, 2), Rectangle(7, 8)};
    
    // 访问对象数组元素
    cout << "rects2[0] area: " << rects2[0].area() << endl;
    cout << "rects3[1] area: " << rects3[1].area() << endl;
    
    // 释放动态分配的内存
    delete[] rects3;
    
    return 0;
}

3.4 注意事项

  • 构造函数的必要性:创建对象数组时,类必须拥有可访问的默认构造函数(除非每个元素都被显式初始化)。

  • 内存泄漏风险:使用new动态分配的对象数组,必须用delete[]释放,不然会造成内存泄漏。

  • 数组大小固定:静态数组的大小在编译时就得确定,无法动态改变。若需要动态调整大小,可以考虑使用std::vector

4、公用数据的保护

为避免在无意中的误操作会改变有关数据的状况,就需要使数据能在一定范围内共享,又要保证它不被任意修改,因此应该把下面几类数据用const定义为常量。

4.1 定义常对象

一般形式:类名 const 对象名 [ (实参表) ]; 或者 const 类名 对象名[ (实参表) ];

定义常对象时,必须同时对之初始化,之后不能改变。

示例:

#include<iostream>
#include<cstring>
using namespace std;
​
class Object
{
    public:
    Object();                   //构造函数
    char appearance[20];        //外貌
    char character[20];         //性格
    char request[20];           //要求
    
    void print() const;         //声明为const成员函数
};
​
Object::Object()
{
    strcpy(appearance,"美丽");
    strcpy(character,"温柔体贴");
    strcpy(request,"善解人意");
}
​
void Object::print() const      // 定义为const成员函数
{
    cout<<"我希望我对象"<<appearance<<"、"<<character<<"、"<<request<<endl;
}
​
int main()
{
    const Object obj;           
    obj.print();
    return 0;
}

说明:

  • 如果一个对象被声明为常对象,则通过该对象只能调用它的常成员函数,而不能调用该对象的普通成员函数(除了由系统自动调用的隐式的构造函数和析构函数)。常成员函数是常对象唯一对外的接口。这是防止普通成员函数会修改常数据中数据成员的值。

  • 常成员函数可以访问常对象中的数据成员,但不允许修改常对象中数据成员的值。

4.2 定义常对象成员

4.2.1 常数据成员

其作用和用法与常变量相似,用关键字const来声明常数据成员。长数据成员的值是不能改变的。

注:只能通过构造函数的参数初始化表对常数据成员进行初始化,任何其他函数都不能对常数据成员赋值。

  • const int hour; //定义hour为长数据成员 不能采用在构造函数中对常数据成员赋初值的方法。以下用法是非法的: Time::Time(){ hour = h;} //因为常数据成员是不能被赋值的

  • 如果在类外定义构造函数应写成以下形式: Time::Time(int h)::hour(h){} //通过参数初始化表对长数据成员hour初始化

  • 常对象的数据成员都是常数据成员,因此在定义常对象时,构造函数只能用参数初始化对长数据成员进行初始化。

4.2.2 常成员函数

如果将成员函数声明为常成员函数,则只能引用非本类中的数据成员,而不能修改它们。

如 void get_time() const; //注意const的位置在函数名和括号之后

声明常成员函数的一般格式为: 类型名 函数名(参数表) const const时函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。

常成员函数对数据成员的引用

数据成员 非const的普通成员函数 const成员函数
非const的普通数据成员 可以引用,也可以改变值 可以引用,但不可以改变值
const数据成员 可以引用,但不可以改变值 可以引用,但不可以改变值
const对象 不允许引用 可以引用,但不可以改变值

怎样利用常成员函数?

  • 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。

  • 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为 const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到“双保险”的作用,切实保证了数据成员不被修改。

  • 如果已定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数(不论这些函数是否会修改对象中的数据)。这是为了保证数据的安全。如果需要访问常对象中的数据成员,可将常对象中所有成员函数都声明为const成员函数,并确保在函数中不修改对象中的数据成员。

  • 不要误认为常对象中的成员函数都是常成员函数。常对象只保证其数据成员是常数据成员,其值不被修改。如果在常对象中的成员函数未加const声明,编译系统把它作为非const 成员函数处理。

  • 还有一点要指出:常成员函数不能调用另一个非const成员函数。

4.2.3 指向对象的常指针

将指针变量声明为const型,这样指针变量始终保持为初值,不能改变,即其指向不变。

如:

Time t1(10,12,15),t2; //定义对象 Time * const ptr1; //const位置在指针变量名前面,指定ptr1时常指针变量 ptr1=&t1; //ptr1指向对象t1,此后不能改变指向 ptr1=&t2; //错误,ptr1不能改变指向

定义指向对象的常指针变量的一般形式: 类名 * const 指针变量名;

特点

  • 必须初始化:定义时必须指定初始值。

  • 不能改变指针的指向:但可以修改对象的值。

注意:指向对象的常指针变量的值不能改变,即始终指向同一个对象,但可以改变其所指向对象的值。

示例:

Point a(1, 2);
Point* const p = &a;  // 常指针,必须初始化
​
p->x = 10;           // 合法:可以修改对象的值
// p = &b;          // 非法:不能改变指针的指向
4.2.4 指向常对象的指针变量

1. 定义

指针本身可以改变(指向其他对象),但不能通过该指针修改所指向的对象。

2. 语法

const 类型* 指针名;  // 或 类型 const* 指针名;

3. 特点

  • 不能修改对象的值:通过指针访问的对象成员是只读的。

  • 可以改变指针的指向:指针可以指向其他对象。

4. 示例

class Point {
public:
    int x, y;
};
​
const Point* p;  // 指向常量对象的指针
Point a(1, 2), b(3, 4);
​
p = &a;         // 合法:可以改变指针指向
// p->x = 10;   // 非法:不能通过指针修改对象
p = &b;         // 合法:可以指向其他对象
4.2.5 对象的常引用

在 C++ 中,对象的常引用(Constant Reference to Object) 是一种特殊的引用类型,它通过 const 修饰符限制对引用对象的修改,既能高效传递对象,又能保证对象的安全性。以下是对其的详细介绍:

1)对象的常引用的定义

对象的常引用是指指向对象的引用被声明为 const,语法形式为:

const 类名& 引用名 = 对象;

它表示:引用本身不能被重新绑定到其他对象(引用的本质特性),且不能通过该引用修改所指向的对象

2)核心特性

  1. 不可修改对象: 通过常引用访问对象时,只能调用对象的 const 成员函数(保证不修改对象),不能修改对象的成员变量或调用非 const 成员函数。

    class Student {
    public:
        int age;
        void setAge(int a) { age = a; }       // 非 const 成员函数(修改对象)
        int getAge() const { return age; }    // const 成员函数(不修改对象)
    };
    ​
    Student s;
    const Student& ref = s;
    ref.getAge();   // 正确:调用 const 成员函数
    // ref.setAge(20); 错误:通过常引用调用非 const 函数(试图修改对象)
    // ref.age = 20;    错误:通过常引用修改成员变量
  2. 引用不可重绑定: 引用一旦初始化绑定到某个对象,就不能再指向其他对象(这是所有引用的共性,与 const 无关,但常引用同样遵循)。

    Student s1, s2;
    const Student& ref = s1;
    // ref = s2; 错误:引用不能重绑定
  3. 高效传递对象: 引用本质是指针的语法糖,传递对象的常引用时,不会像值传递那样复制整个对象,能节省内存和时间,尤其适合大型对象。

3)典型应用场景

  1. 作为函数参数

最常见的用途是在函数参数中使用常引用,既能避免对象的拷贝(提高效率),又能防止函数内部意外修改传入的对象。

// 函数参数为对象的常引用
void printStudent(const Student& s) {
    cout << "年龄:" << s.getAge() << endl;  // 正确:调用 const 函数
    // s.setAge(20); 错误:试图修改对象
}

int main() {
    Student s;
    s.age = 18;
    printStudent(s);  // 传递对象的常引用,无需拷贝
    return 0;
}
  1. 作为函数返回值

当函数需要返回对象但不希望外部修改该对象时,可返回常引用(通常用于返回类的内部成员对象)。

class School {
private:
    Student headmaster;  // 内部对象
public:
    // 返回校长的常引用,外部只能读取,不能修改
    const Student& getHeadmaster() const {
        return headmaster;
    }
};

int main() {
    School sch;
    const Student& hm = sch.getHeadmaster();  // 接收常引用
    hm.getAge();  // 正确
    // hm.setAge(50); 错误:不能修改
    return 0;
}
  1. 临时对象的引用

C++ 允许将常引用绑定到临时对象(值传递产生的匿名对象),延长临时对象的生命周期(直到常引用作用域结束)。

Student createStudent() {
    return Student();  // 返回临时对象
}

int main() {
    // 常引用绑定到临时对象,临时对象生命周期延长至 ref 作用域结束
    const Student& ref = createStudent();
    ref.getAge();  // 正确:访问临时对象
    return 0;
}

4)与普通引用、值传递的对比

方式 能否修改对象 是否拷贝对象 适用场景
普通引用(& 需要修改传入对象时
常引用(const & 只需读取对象,无需修改时
值传递 能(仅副本) 需要修改对象的副本时

结论:当函数需要传递对象且无需修改时,优先使用常引用(兼顾效率和安全性)。

5)注意事项

  1. 常引用只能绑定到 const 或非 const 对象,但不能用普通引用绑定到 const 对象。

    const Student s;
    const Student& ref1 = s;  // 正确:常引用绑定到 const 对象
    Student s2;
    const Student& ref2 = s2;  // 正确:常引用绑定到非 const 对象
    // Student& ref3 = s; 错误:普通引用不能绑定到 const 对象
  2. 常引用只能调用 const 成员函数,确保对象不被修改。若调用非 const 函数,编译器会直接报错。

  3. 避免返回局部对象的常引用:局部对象在函数结束后会被销毁,返回其引用会导致悬空引用(访问无效内存)。

    // 错误示例:返回局部对象的引用
    const Student& badFunc() {
        Student s;  // 局部对象,函数结束后销毁
        return s;   // 危险:返回悬空引用
    }

6)总结

对象的常引用是 C++ 中高效且安全的对象传递方式,核心价值在于:

  • 高效性:避免对象拷贝,尤其适合大型对象。

  • 安全性:防止通过引用意外修改对象,配合 const 成员函数形成保护机制。

  • 灵活性:可绑定到普通对象、const 对象甚至临时对象。

在实际开发中,函数参数传递对象时,若无需修改对象,应始终优先使用常引用,这是编写高质量 C++ 代码的重要实践。

4.2.6 const型数据的小结

为便于区分 C++ 中与对象相关的 const 型数据,通过具体形式梳理其含义,以 Time 类为场景示例:

语法形式 含 义 解 释
Time const t1; t1常对象,其内部数据成员的值在任何情况下都不能被修改(对象状态固定)
void Time::fun() const; funTime 类的常成员函数,可访问类数据成员,但承诺不会修改数据成员(逻辑只读)
Time * const p; p指向 Time 类对象的常指针,指针本身的指向(存储的地址值)不能改变,但可通过指针修改对象内容(若对象允许)
const Time * p; p指向 Time 类常对象的指针,指针指向的对象内容不能通过 p 修改(对象只读,但指针可重新指向其他对象)
const Time &tl = t; tlTime 类对象 t 的常引用tlt 共享存储空间,且通过 tl 无法修改 t 的值

补充说明

const 相关语法是 C++ 类型安全的重要基础,核心作用是明确约束 “哪些内容可修改、哪些不可修改”

  • 常对象 / 常引用 / 指向常对象的指针:限制对象内容不可变;

  • 指向对象的常指针:限制指针指向不可变;

  • 常成员函数:限制成员函数对对象状态的修改。

实际编码中,合理使用 const 可避免意外修改数据、提升代码可读性与安全性。初期不必死记,结合场景实践、回头查阅即可逐步掌握。

4.2.7 mutable关键字

在 C++ 中,mutable 是一个关键字,用于修饰类的成员变量。它允许该成员变量即使在 const 对象const 成员函数 中也能被修改。这为类的实现提供了更大的灵活性,尤其是在需要维护内部状态而不影响对象外部常量性的场景中。

1、mutable 的核心作用突破 const 的限制

const 成员函数中,默认不能修改对象的任何成员变量。但被 mutable 修饰的成员变量是个例外,它们可以在 const 函数中被修改。

2、语法与使用场景

2.1. 修饰成员变量

class Example {
private:
    mutable int counter;  // 即使在 const 函数中也能修改
    int value;            // 普通成员变量,在 const 函数中不可修改
​
public:
    void increment() const {
        counter++;  // 合法:mutable 成员可在 const 函数中修改
        // value++;  // 非法:普通成员不能在 const 函数中修改
    }
};

2.2. 典型应用场景

  • 缓存计算结果:在 const 函数中更新缓存值。

    class Circle {
    private:
        double radius;
        mutable double areaCache;  // 缓存面积,避免重复计算
        mutable bool cacheValid;   // 缓存有效性标记
    ​
    public:
        double getArea() const {
            if (!cacheValid) {
                areaCache = 3.14159 * radius * radius;
                cacheValid = true;  // 修改 mutable 成员
            }
            return areaCache;
        }
    };
  • 实现线程安全:在 const 函数中更新锁或原子计数器。

    class ThreadSafe {
    private:
        mutable std::mutex mtx;  // 保护共享资源的锁
        int data;
    
    public:
        int getData() const {
            std::lock_guard<std::mutex> lock(mtx);  // 锁操作修改 mutable 成员
            return data;
        }
    };

3、注意事项

  • 仅用于成员变量mutable 只能修饰类的非静态成员变量,不能用于全局变量、函数参数等。

  • 逻辑常量性:使用 mutable 后,对象的外部状态仍保持常量性(对外部观察者而言不可变),但内部状态可能改变。

  • const 的关系:

    • mutable 成员可以在任何成员函数中被修改,包括 const 函数。

    • 普通成员在 const 函数中不可修改,除非通过 const_cast(不推荐)。

4、对比:mutable vs const_cast

方法 作用 风险
mutable 声明成员变量可在 const 函数中修改,是语言层面的显式支持。 无(只要修改不影响对象外部常量性)。
const_cast const 函数中强制移除对象的 const 属性,修改普通成员变量。 破坏类型系统,可能导致未定义行为。

5、总结

mutable 是 C++ 中一个微妙但强大的特性,它允许在保持对象外部常量性的同时,灵活管理内部状态。合理使用 mutable 可以提高代码的性能(如缓存优化)和安全性(如线程同步),但需避免滥用,确保修改行为符合对象的逻辑常量性。

5、对象的动态建立和释放

在 C++ 中,对象的动态建立和释放主要通过newdelete操作符来实现,这对于灵活管理内存,特别是在运行时根据实际需求创建和销毁对象非常重要。以下是详细介绍:

作用:

维度 具体说明 典型场景
内存分配灵活性 运行时按需分配内存,突破编译期静态分配的限制,内存大小 / 对象数量由程序逻辑决定。 处理用户输入动态创建对象(如根据文件行数创建对应数量的数据对象);动态数据结构(链表、树)节点创建。
生命周期控制 对象生命周期由开发者显式控制(new 分配后,delete/ 智能指针释放前持续存在),可跨作用域传递使用。 函数 A 创建对象,传递给函数 B 使用,函数 A 结束后对象仍可用;长生命周期对象(全局缓存、配置对象)。
动态数据结构基础 是链表、树、图等动态数据结构的实现前提,节点数量 / 连接关系运行时确定,依赖动态创建 / 释放对象调整结构。 链表新增节点(new 分配节点)、删除节点(delete 释放节点);二叉树动态构建与销毁。
内存效率优化 仅在需要时分配内存,不需要时立即释放(delete/ 智能指针自动释放),避免静态对象 “占着不用也不释放” 的内存浪费。 嵌入式系统(内存资源紧张);高频创建 / 销毁对象的场景(如网络请求临时对象)。
多态与动态绑定支持 配合基类指针实现多态:new 创建派生类对象,基类指针指向它,运行时根据实际类型调用方法(动态绑定)。 图形库中,Shape* s = new Circle(),统一调用 s->draw() 实现圆形、矩形等不同绘制逻辑(多态)。
栈内存限制突破 静态对象存在栈中,空间有限(通常几 MB),动态对象在堆中分配(GB 级空间),适合大型 / 数量多的对象。 处理大文件数据(需创建大量对象存储内容);高分辨率图像像素对象动态分配。
风险与现代方案 手动 new/delete 易引发内存泄漏(忘释放)、野指针(释放后继续用);现代 C++ 推荐智能指针(unique_ptr/shared_ptr)自动管理。 智能指针自动释放内存,unique_ptr 独占对象,shared_ptr 共享对象,简化内存管理并规避风险。

关键补充

  • 若追求极致简洁 / 无额外开销,用 new/delete 但需严格管理;

  • 若要规避内存问题,优先用智能指针(C++11+ 主流实践);

  • 动态管理的核心价值是让内存分配 / 释放与程序逻辑 “按需匹配”,平衡灵活性与资源效率。

5.1动态建立对象

5.1.1使用new操作符创建单个对象

使用new操作符可以在堆内存中动态分配内存来创建对象,它会调用对象的构造函数进行初始化。语法格式为:

类名 *指针变量 = new 类名(构造函数参数);

示例:

#include <iostream>
using namespace std;
​
class Time {
public:
    int hour;
    int minute;
    int sec;
    Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), sec(s) {}
};
​
int main() {
    // 使用new动态创建Time对象
    Time *t = new Time(10, 20, 30);
    cout << "动态创建的对象 - 时间:" << t->hour << ":" << t->minute << ":" << t->sec << endl;
    return 0;
}

在上述代码中,new Time(10, 20, 30)在堆内存中分配了一个Time对象所需的内存空间,并调用了Time类带有参数的构造函数进行初始化,然后将对象的地址赋值给指针t

5.1.2使用new操作符创建对象数组

可以使用new操作符在堆内存中动态创建对象数组,语法格式为:

类名 *指针变量 = new 类名[数组大小];

示例:

#include <iostream>
using namespace std;
​
class Student {
public:
    int id;
    Student(int i = 0) : id(i) {}
};
​
int main() {
    // 使用new动态创建包含3个Student对象的数组
    Student *students = new Student[3];
    for (int i = 0; i < 3; ++i) {
        students[i].id = i + 1;
        cout << "学生" << i + 1 << "的ID:" << students[i].id << endl;
    }
    return 0;
}

这里new Student[3]在堆内存中分配了能存储 3 个Student对象的连续内存空间,会调用Student类的默认构造函数对每个元素进行初始化。

5.2动态释放对象

5.2.1. 使用delete操作符释放单个对象

当使用new创建的单个对象不再需要时,要使用delete操作符来释放其占用的堆内存,以避免内存泄漏。delete会调用对象的析构函数来清理资源。语法格式为:

delete 指针变量;

示例(承接前面创建单个Time对象的例子):

int main() {
    Time *t = new Time(10, 20, 30);
    cout << "动态创建的对象 - 时间:" << t->hour << ":" << t->minute << ":" << t->sec << endl;
    // 使用delete释放对象
    delete t;
    t = nullptr;  // 建议将指针置空,防止野指针
    return 0;
}

delete t会调用Time对象的析构函数(如果有定义的话),然后释放该对象占用的堆内存。将指针置空是为了避免后续不小心使用已经释放的指针(野指针)。

5.2.2. 使用delete[]操作符释放对象数组

对于使用new创建的对象数组,需要使用delete[]操作符来释放,它会为数组中的每个元素调用析构函数。语法格式为:

delete[] 指针变量;

示例(承接前面创建Student对象数组的例子):

int main() {
    Student *students = new Student[3];
    for (int i = 0; i < 3; ++i) {
        students[i].id = i + 1;
        cout << "学生" << i + 1 << "的ID:" << students[i].id << endl;
    }
    // 使用delete[]释放对象数组
    delete[] students;
    students = nullptr;
    return 0;
}

delete[] students会依次调用数组中 3 个Student对象的析构函数,然后释放整个数组占用的堆内存。

5.3智能指针与动态内存管理(C++11 及以后)

为了更安全、方便地管理动态分配的内存,C++11 引入了智能指针(std::unique_ptrstd::shared_ptrstd::weak_ptr)。它们会在合适的时候自动释放所管理的内存,从而避免了手动管理内存时容易出现的内存泄漏和悬空指针等问题。

std::unique_ptr

独占式拥有对象,同一时刻只能有一个std::unique_ptr指向对象,当std::unique_ptr离开作用域或被重置时,会自动释放对象。示例:

#include <iostream>
#include <memory>
​
class Resource {
public:
    Resource() { std::cout << "Resource created" << std::endl; }
    ~Resource() { std::cout << "Resource destroyed" << std::endl; }
};
​
int main() {
    std::unique_ptr<Resource> ptr = std::make_unique<Resource>();
    // 离开作用域时,Resource对象会自动被释放
    return 0;
}

std::shared_ptr

共享式拥有对象,多个std::shared_ptr可以指向同一个对象,通过引用计数来管理对象的生命周期,当引用计数降为 0 时,对象会被自动释放。示例:

#include <iostream>
#include <memory>
​
class Resource {
public:
    Resource() { std::cout << "Resource created" << std::endl; }
    ~Resource() { std::cout << "Resource destroyed" << std::endl; }
};
​
int main() {
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
    std::shared_ptr<Resource> ptr2 = ptr1;  // 引用计数增加
    // 当ptr1和ptr2都离开作用域时,引用计数降为0,Resource对象被释放
    return 0;
}

智能指针的使用大大简化了动态内存管理,提高了代码的安全性和可读性,在现代 C++ 编程中被广泛推荐。

6、对象的赋值和复制

6.1 对象的赋值(Assignment)

定义: 将一个已存在对象的数据赋给另一个已存在的对象,通过重载赋值运算符(operator=)实现。 语法

对象1 = 对象2;  // 调用赋值运算符函数
6.1.1 默认赋值行为

若未自定义赋值运算符,编译器会生成默认的赋值运算符,执行成员逐个复制(浅拷贝):

class Point {
public:
    int x, y;
};
​
Point p1(1, 2), p2(3, 4);
p1 = p2;  // 默认赋值:p1.x = p2.x; p1.y = p2.y;
6.1.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;
    }
};
6.1.3 赋值运算符的注意事项
  • 返回引用:为支持链式赋值(如 a = b = c),应返回 *this 的引用。

  • 处理自我赋值:检查 this == &other,避免释放自身资源后再访问。

  • 深拷贝与浅拷贝:对动态资源需深拷贝(重新分配内存并复制内容)。

6.1.4说明
  • 对象的复制只是对其中数据成员的复制,而不对成员函数复制。数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一个对象的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段,不需要也无法对它们赋值。

  • 类的数据成员中不能包括动态分配的数据,否则在负值时可能出现严重后果。

6.2 对象的复制(Copy)

定义: 通过构造函数创建一个新对象,其初始值与已存在对象相同,主要通过拷贝构造函数实现。 语法

类名 对象2(对象1);        // 显式调用拷贝构造函数
类名 对象2 = 对象1;       // 隐式调用拷贝构造函数(初始化语句)
6.2.1 默认拷贝构造函数

若未自定义拷贝构造函数,编译器会生成默认版本,执行成员逐个复制(浅拷贝):

class Point {
public:
    int x, y;
};
​
Point p1(1, 2);
Point p2(p1);  // 默认拷贝构造:p2.x = p1.x; p2.y = p1.y;
6.2.2 自定义拷贝构造函数

当类包含动态资源时,需自定义拷贝构造函数以避免浅拷贝问题: 示例

class String {
private:
    char* data;
public:
    // 自定义拷贝构造函数(深拷贝)
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
};
6.2.3 拷贝构造函数的触发场景
  • 显式初始化Point p2(p1);

  • 函数值传递void func(Point p) { ... } 调用时复制实参。

  • 函数返回值Point func() { return p; } 返回对象时复制。

6.3 赋值与复制的核心区别

维度 对象赋值(Assignment) 对象复制(Copy)
本质 对已存在对象的数据更新 创建新对象并初始化为已有对象的值
触发时机 对象已存在,执行 对象1 = 对象2; 创建新对象时,如 类名 对象2(对象1);
核心函数 赋值运算符 operator= 拷贝构造函数 类名(const 类名&)
资源管理 可能需要先释放原有资源(如动态内存)再复制 直接分配新资源并复制内容

6.4深拷贝 vs 浅拷贝

类型 实现方式 适用场景 风险
浅拷贝 复制对象的所有成员(包括指针值) 类中无动态资源(如指针、文件句柄) 多个对象共享同一资源,析构时重复释放导致崩溃
深拷贝 复制对象时为动态资源重新分配内存并复制内容 类中包含动态资源 需手动管理内存,实现较复杂

6.5 移动语义(C++11+)

为优化临时对象的复制开销,C++11 引入移动构造函数移动赋值运算符,通过转移资源所有权(而非复制)提高效率: 示例

class String {
public:
    // 移动构造函数
    String(String&& other) noexcept : data(other.data) {
        other.data = nullptr;  // 转移所有权,避免原对象析构时释放资源
    }
};

6.6 总结与最佳实践

  1. 默认行为:若无动态资源,默认的赋值运算符和拷贝构造函数通常足够。

  2. 自定义规则:若类包含动态资源,需同时自定义拷贝构造函数赋值运算符析构函数(合称 “三 / 五法则”)。

  3. 优先使用移动语义:对临时对象(如函数返回值),使用移动构造 / 赋值避免深拷贝。

  4. 禁用复制 / 赋值:若对象不可复制(如单例模式),可将拷贝构造函数和赋值运算符声明为 privatedelete

7、不同对象间实现数据共享

如果想在同类的多个对象之间实现数据共享,不要用全局对象,可以用静态的数据成员。

7.1 把数据成员定义为静态

在 C++ 里,静态数据成员是类的一种特殊数据成员,它被该类的所有对象共享,而非某个对象独有。它以static关键字开头。

例如: class Box {public: int volume(); private: static int height; //定义height为静态数据成员 int width; int lengh; };

如果希望同类的各成员中的数据成员的值是一样的,就可以把它定义为静态数据成员,这样它就为各对象所共有,所有对象都可以引用它。静态数据成员在内存中只占一份空间(而非每个对象都分别为它保留一份空间)。每个对象都可以引用这个静态数据成员。静态数据成员的值对所有的对象都是一样的。如果改变它的值,则在各对象中这个数据成员的值都同时改变了。这样可以节约空间,提高效率。

说明:

①如果声明了类而未定义对象,则类的一般数据成员是不占用内存空间的,只有在定义对象是,才为对象的数据成员分配空间。但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空概念。静态数据成员是在所有对象之外单独开辟空间。只要在类中指定了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。 在一个类中可以有一个或多个静态数据成员,所有对象都共享这些静态数据成员,都可以引用它。

②在C语言中已了解静态变量的概念:如果在一个函数中定义了静态变量,在函数结束时该静态变量并不释放,仍然存在并保留其值。 C++的静态数据成员也是类似的,它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员时在对象建立时分配空间按,在对象撤销时释放)。静态数据成员是在程序编译时被分配并预留空间的,开始运行程序时就占用分配的内存,到程序结束时才释放空间。

③公用的静态数据成员可以初始化,但只能在类体外进行初始化。

一般形式:数据类型 类名::静态数据成员名=初值; 例如: int Box::height=10;

只在类体中声明静态数据成员时加static,不必在初始化语句中加static。

注意:不能用构造函数的初始化列表对静态成员初始化,如果未对静态数据成员赋初值,则编译系统会自动赋予初值。

④静态数据成员既可以通过对象名引用,也可以通过类名来使用。 示例:

#include<iostream>
using namespace std;
class Box
{
    public:
    Box(int,int);
    int volume();
    static int height;
    int width;
    int length;
};
Box::Box(int w,int len)
{
    width=w;
    length=len;
}
int Box::volume()
{
    return height*width*length;
}
int Box::height=10;
​
int main()
{
    Box a(15,20),b(20,30);      //建立两个对象
    cout<<a.height<<endl;       //通过对象名a来引用静态变量
    cout<<b.height<<endl;
    cout<<Box::height<<endl;    //通过类名引用静态数据成员
    cout<<a.volume()<<endl; 
    return 0;
}

注意:在类外可以通对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员。即没有定义类对象,也可以通过类名引用静态数据成员。这说明静态数据成员不是属于对象的,而是属于类的,但类的对象可以引用它。 如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用。

⑤有了静态数据成员同类的各对象之间的数据有了沟通的渠道,实现类的数据共享,不需要全局变量。全局变量破坏了封装性原则,不符合面向对象程序的要求。

但也要注意共用静态数据变量与全局变量的不同,静态数据成员的作用域只限于定义该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内)。在此作用域内,可以通过类名和域运算符“::”引用静态数据成员,而不论类对象是否存在。

1.静态数据成员的主要特性:

  1. 内存分配情况:静态数据成员的内存是在程序开始运行时分配的,而且只分配一次,并非每次创建类对象时都分配。

  2. 声明与定义方式:在类的定义中要对静态数据成员进行声明,不过它的定义必须在类的外部完成。下面是一个示例:

class MyClass {
public:
    static int count; // 类内声明静态数据成员
};
​
int MyClass::count = 0; // 类外定义并初始化

2.访问途径:可以通过类名直接访问静态数据成员,也能通过对象来访问。例如:

MyClass::count = 10; // 直接通过类名访问
MyClass obj;
obj.count = 20; // 通过对象访问(不推荐这种方式)
  1. 初始化规则:静态数据成员一般要在类外进行初始化。不过,如果是const static整型(像intcharlong等),可以在类内直接初始化。示例如下:

class MyClass {
public:
    static const int MAX = 100; // 类内直接初始化const static整型
};

4.应用场景:静态数据成员常用于统计类创建的对象数量,或者存储类的全局配置信息等。

静态数据成员能够实现同一类的不同对象之间的数据共享,有助于减少内存占用。不过在多线程环境中使用时,要注意对静态数据成员的访问同步问题。

7.2 用静态数据成员函数访问静态数据成员

成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数。如

static int volume;

和静态数据成员一样,静态成员函数是类的一部分而不是对象的一部分。如果要在类外调用公用的静态成员函数,要用类名和域运算符“::”,如

Box::volume();

静态函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。

静态成员函数不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。它没有指向某一对象,就没法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。

静态成员函数与非静态成员函数的本质区别: 非静态成员函数有this指针,而静态成员函数没有this指针。因此静态成员函数不能访问本类中的非静态成员。

静态成员函数可以直接引用本类中的静态成员,因为静态成员同样时属于类的,可以直接引用。在C++程序中,静态成员函数主要是用来访问静态数据成员,而非访问非静态数据成员。

案例:给定若干同学的数据(包括学号,年龄和成绩),要求统计学生平均成绩。使用静态成员函数。

#include<iostream>
using namespace std;
​
class Student{
    public:
    Student(int n, int a, float s): num(n), age(a), score(s) {};  
    void total();                          // 声明成员函数
    static float average();                // 声明静态成员函数
    private:
    int num;
    int age;
    float score;
    static float sum;
    static int count;
};
​
void Student::total()
{
    sum += score;
    count++;
}
​
float Student::average()
{
    return (sum/count);
}
​
float Student::sum = 0;
int Student::count = 0;
​
int main()
{       
    Student stud[3] = {                  //定义对象数组并初始化
        Student(1001, 18, 70),
        Student(1002, 19, 78),
        Student(1005, 20, 98)
    };
    
    int n;
    cout << "please input the number of students:";
    cin >> n;
    
    for(int i = 0; i < n; i++)
        stud[i].total();                
    
    cout << "the average score of " << n << " students is " << Student::average() << endl;  
    return 0;
}

一些知识:

1. 静态数据成员

静态数据成员是类的所有对象共享的一个数据成员,它在类的定义中进行声明,不过需要在类的外部进行初始化。其基本语法如下:

class MyClass {
public:
 static int count; // 声明静态数据成员
};
​
// 类外初始化静态数据成员
int MyClass::count = 0;

2. 静态成员函数

静态成员函数同样属于类,并非属于类的对象。它只能访问类的静态数据成员或者调用其他静态成员函数,不能访问类的非静态成员。静态成员函数的基本语法如下:

class MyClass {
public:
 static void incrementCount() { // 声明静态成员函数
     count++; // 可以访问静态数据成员
 }
 static int getCount(); // 声明静态成员函数
};
​
// 类外定义静态成员函数
int MyClass::getCount() {
 return count;
}

3. 静态成员函数访问静态数据成员的实例

下面通过一个完整的示例,展示如何利用静态成员函数来访问静态数据成员:

#include <iostream>
using namespace std;
​
class Student {
private:
 static int totalStudents; // 声明静态数据成员:记录学生总数
 string name;
 int age;
​
public:
 Student(string n, int a) : name(n), age(a) {
     totalStudents++; // 每次创建新对象时,学生总数加1
 }
​
 // 静态成员函数:获取学生总数
 static int getTotalStudents() {
     return totalStudents;
 }
};
​
// 类外初始化静态数据成员
int Student::totalStudents = 0;
​
int main() {
 Student s1("Alice", 20);
 Student s2("Bob", 21);
​
 // 通过类名直接调用静态成员函数
 cout << "Total students: " << Student::getTotalStudents() << endl;
​
 return 0;
}

4. 关键要点

  • 访问方式:静态成员函数能够直接访问静态数据成员,无需创建类的对象。

  • 调用方式:可以通过类名加作用域解析运算符(类名::静态成员函数())来调用静态成员函数,也可以通过类的对象来调用。

  • 限制条件:静态成员函数不能访问类的非静态成员,这是因为非静态成员需要通过对象来访问,而静态成员函数不与任何对象绑定。

  • 初始化要求:静态数据成员必须在类的外部进行初始化,否则会引发链接错误。

通过静态成员函数访问静态数据成员,能够在不创建对象的情况下管理和操作类级别的数据,这在实现计数器、配置管理等功能时十分实用。

8、允许访问私有数据的”朋友“

8.1 友元

在 C++ 中,友元(Friend) 是一种允许一个类或函数访问另一个类的私有(private)和保护(protected)成员的机制。使用关键字friend进行声明。

尽管它打破了类的封装性,但在某些场景下(如运算符重载、数据共享等)非常有用。

核心概念

  1. 友元函数

    • 非类成员函数,但被授权访问类的私有 / 保护成员。

    • 使用 friend 关键字在类中声明。

    class MyClass {
    private:
        int secret;
    public:
        friend void friendFunc(MyClass obj);  // 声明友元函数
    };
    ​
    void friendFunc(MyClass obj) {
        cout << obj.secret;  // 合法:友元函数可访问私有成员
    }
  2. 友元类

    • 一个类被授权访问另一个类的私有 / 保护成员。

    class A {
    private:
        int x;
    friend class B;  // B是A的友元类,B的所有成员函数可访问A的私有成员
    };
    
    class B {
    public:
        void accessA(A a) { cout << a.x; }  // 合法
    };
  3. 友元成员函数

    • 一个类的成员函数成为另一个类的友元。

    class B;  // 前向声明
    
    class A {
    private:
        int x;
    friend void B::func(A a);  // B的成员函数func是A的友元
    };
    
    class B {
    public:
        void func(A a) { cout << a.x; }  // 合法
    };

关键特性

  • 单向性:友元关系不可传递。若 AB 的友元,B 不一定是 A 的友元。

  • 非继承性:友元关系不能被派生类继承。

  • 谨慎使用:友元会破坏类的封装性,应仅在必要时使用(如接口与实现分离的场景)。

典型应用场景

  1. 运算符重载(如重载 << 输出流运算符)

    class Point {
    private:
        int x, y;
    public:
        friend ostream& operator<<(ostream& os, const Point& p);
    };
    
    ostream& operator<<(ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";  // 访问私有成员
        return os;
    }
  2. 数据共享优化

    • 当两个类需要紧密协作时,避免通过公共接口访问数据的开销。

注意事项

  • 避免滥用:过度使用友元会导致代码耦合度增加,可维护性降低。

  • 替代方案:优先考虑通过公共接口(如 getter/setter)访问数据,仅在性能或设计必需时使用友元。

8.1.1 可以访问私有数据的友元函数

如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数),在类体中用friend对其进行声明,此函数就称为本类的友元函数。友元函数可以访问这个类中的私有成员。

将普通函数声明为友元函数

示例:

#include<iostream>
using namespace std;
class Time
{
    public:
    Time(int,int,int);              //声明构造函数
    friend void display(Time &);    //声明display函数为Time类的友元函数
    private:
    int hour;
    int minute;
    int sec;
};
​
Time::Time(int h,int m,int s)
{
    hour=h;
    minute=m;
    sec=s;
}
​
void display(Time &t)           //这是普通函数,形参t时Time类对象的引用
{
    cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;
}
​
int main()
{
    Time t1(1,2,3);
    display(t1);
    return 0;
}

② 用友元成员函数访问私有数据

friend函数不经可以是一般函数(非成员函数),而且还可以是另一个类中的成员函数。

示例:有一个日期(Date)类的对象和一个时间(Time)类的对象,均已指定了内容,要求一次输出其中的日期和时间。

#include<iostream>
using namespace std;
class Date;							//对Date类的提前引用声明
class Time
{
    public:
    Time(int,int,int);				//声明构造函数
    void display(Date &);			//disolay是成员函数,形参是Date类对象的引用
    private:
    int hour;
    int minute;
    int sec;
};
class Date
{
    public:
    Date(int,int,int);				//声明构造函数
    friend void Time::display(Date &);		//声明Time中display函数为本类的友元成员函数
    private:
    int month;
    int day;
    int year;
};

Time::Time(int h,int m,int s)		//定义类Time的构造函数
{
    hour=h;
    minute=m;
    sec=s;
}

Date::Date(int y,int m,int d)
{
    year-y;
    month=m;
    day=d;
}
void Time::display(Date &d)		//dispay的作用是输出年、月、日和时分秒
{
    cout<<"Now is "<<d.year<<"/"<<d.month<<"/"<<d.day<<" "		//引用Date类中的私有数据
        <<hour<<":"<<minute<<":"<<sec<<endl;					//引用本类对象中的私有数据
}

int main()
{
    Time t1(1,2,3);
    Date d1(2025,7,30);
    t1.display(d1);				//调用t1中的display函数,实参是Date类对象
    return 0;
}

请注意在本程序中调用友元函数访问有关类的私有数据方法:

  1. 在函数名display前要加display所在的对象名

  2. display成员函数的实参是Date类对象d1,否则就不能访问对象d1中的私有数据。

  3. display成员函数中引用Date类私有数据时必须加上对象名,如d,month。

类的提前引用声明:

在 C++ 中,类的提前引用声明(Forward Declaration) 是一种在正式定义类之前声明类名的机制。它允许在未完整定义类的情况下引用该类,从而解决类之间的循环依赖问题或减少头文件包含。

核心概念

  1. 语法 使用 class ClassName; 声明一个类名,而不提供完整定义。

    class MyClass;  // 提前声明MyClass
  2. 作用

    • 解决循环依赖:当两个类相互引用时,无法直接完整定义。

    • 减少头文件包含:在声明指针或引用时避免引入完整头文件,提高编译效率

一个函数(包括普通函数和成员函数)可以被多个类声明为友元,这样就可以引用多个类中的私有数据。

8.1.2 可以访问私有数据的友元类

不仅可以将一个函数声明为一个类的友元,而且可以将一个类(类A)声明为另一个类(类B)的友元。这时类B就是类A的友元类。

友元类B中的所有函数都是类A的友元函数,可以访问类A中的所有成员。

声明友元类的一般形式:friend 类名;

示例:

#include <iostream>
#include <string>
using namespace std;
​
// 前向声明
class Bank;
​
// 账户类:存储用户信息和余额
class Account {
private:
    string accountHolder;  // 账户持有人
    double balance;        // 账户余额
​
public:
    // 构造函数
    Account(string holder, double initialBalance) 
        : accountHolder(holder), balance(initialBalance) {}
​
    // 普通成员函数:存款
    void deposit(double amount) {
        balance += amount;
        cout << "存款成功,当前余额: " << balance << endl;
    }
​
    // 声明Bank为友元类
    friend class Bank;
};
​
// 银行类:管理账户
class Bank {
private:
    string bankName;
​
public:
    Bank(string name) : bankName(name) {}
​
    // 友元类可以访问Account的私有成员
    void withdraw(Account& account, double amount) {
        if (account.balance >= amount) {
            account.balance -= amount;  // 直接访问Account的私有成员
            cout << "取款成功,当前余额: " << account.balance << endl;
        } else {
            cout << "余额不足,取款失败" << endl;
        }
    }
​
    // 查询账户信息(直接访问私有成员)
    void showAccountInfo(const Account& account) {
        cout << "银行: " << bankName << endl;
        cout << "账户持有人: " << account.accountHolder << endl;
        cout << "账户余额: " << account.balance << endl;
    }
};
​
int main() {
    // 创建账户
    Account userAccount("张三", 1000.0);
    
    // 创建银行
    Bank myBank("ABC银行");
​
    // 通过Bank类操作Account(友元权限)
    myBank.showAccountInfo(userAccount);
    myBank.withdraw(userAccount, 500.0);
    userAccount.deposit(200.0);
    myBank.showAccountInfo(userAccount);
​
    return 0;
}

注意:

关于友元类的两点说明:

  1. 友元的关系时单向的而非双向的 如果声明了类B时类A的友元类,不等于类A是类B的友元类,类A中的成员函数不能访问类B中的私有数据。

  2. 友元的关系是不可以传递的 如果类B是类A的友元类,类C是类B的友元类,不等于类C是类A的友元类

友元有助于数据共享,能提高程序的效率。但不要过多的使用友元,只有在使用它能使程序精炼,较大地提高程序效率四才用友元。即寻找数据共享与信息屏蔽之间的一个平衡点。

8.1.3 全局函数作友元

在 C++ 中,全局函数作为友元(Friend Function) 是一种允许普通函数访问某个类的私有(private)和保护(protected)成员的机制。尽管全局函数不属于任何类,但通过友元声明,它可以突破类的访问限制。

核心概念

  1. 语法 在类中使用 friend 关键字声明全局函数,格式为:

    friend 返回类型 函数名(参数列表);
  2. 关键特性

    • 全局函数不是类的成员函数,但可访问类的私有 / 保护成员。

    • 友元关系是单向的,不具有传递性。

    • 通常用于需要访问类内部状态的辅助函数(如运算符重载、工具函数)。

典型应用场景

  1. 运算符重载(如输入 / 输出流运算符)

    class Point {
    private:
        int x, y;
    public:
        Point(int x = 0, int y = 0) : x(x), y(y) {}
        friend ostream& operator<<(ostream& os, const Point& p);  // 友元声明
        friend istream& operator>>(istream& is, Point& p);        // 友元声明
    };
    ​
    // 重载输出流运算符
    ostream& operator<<(ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";  // 直接访问私有成员
        return os;
    }
    ​
    // 重载输入流运算符
    istream& operator>>(istream& is, Point& p) {
        is >> p.x >> p.y;  // 直接访问私有成员
        return is;
    }
  2. 工具函数访问类内部状态

    class Rectangle {
    private:
        int width, height;
    public:
        Rectangle(int w, int h) : width(w), height(h) {}
        friend int calculateArea(const Rectangle& rect);  // 友元声明
    };
    
    // 全局函数:计算矩形面积
    int calculateArea(const Rectangle& rect) {
        return rect.width * rect.height;  // 直接访问私有成员
    }

使用注意事项

  1. 声明位置

    • 友元声明通常放在类的开头(public、private 或 protected 部分均可,效果相同)。

  2. 函数定义

    • 友元函数的定义通常在类外部,不需要加 friend 关键字。

  3. 命名空间与作用域

    • 如果类位于命名空间中,友元函数需显式指定命名空间:

      namespace MyNS {
          class MyClass {
              friend void func();  // 声明全局函数
          };
      }
      
      // 定义时需指定命名空间
      void MyNS::func() { /* ... */ }
  4. 避免滥用

    • 友元会破坏类的封装性,建议仅在必要时使用(如无法通过公共接口实现功能)。

示例代码

以下是一个完整示例,展示全局函数作为友元访问类的私有成员:

#include <iostream>
using namespace std;

class Date {
private:
    int year, month, day;
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {}

    // 声明全局函数为友元
    friend bool isLeapYear(const Date& date);
    friend void printDate(const Date& date);
};

// 判断是否为闰年(需访问私有成员year)
bool isLeapYear(const Date& date) {
    return (date.year % 4 == 0 && date.year % 100 != 0) || (date.year % 400 == 0);
}

// 打印日期(需访问私有成员year, month, day)
void printDate(const Date& date) {
    cout << date.year << "-" << date.month << "-" << date.day;
    if (isLeapYear(date)) cout << " (闰年)";
    cout << endl;
}

int main() {
    Date d(2024, 2, 29);
    printDate(d);  // 输出: 2024-2-29 (闰年)

    Date d2(2023, 10, 15);
    printDate(d2);  // 输出: 2023-10-15
    return 0;
}

总结

全局函数作为友元是 C++ 提供的一种灵活机制,允许特定函数访问类的私有状态,常用于实现运算符重载、工具函数等场景。但需谨慎使用,确保代码的封装性和可维护性。

9、类模板

9.1 函数模板

为解决函数体完全相同,只是形参不同也需要分别定义麻烦,在 C++ 中,函数模板(Function Template) 是一种通用编程工具,允许创建不依赖于具体数据类型的函数。通过模板,你可以定义一个函数框架,在调用时自动或显式指定具体类型,从而实现代码复用。

所谓的函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需要在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同的函数功能。

定义函数模板的一般形式: template<typename T> 通用函数定义

template<class T> 通用函数定义

template的含义是“模板”,<>当中先写关键字typename(或class),后面跟一个类型参数T,这个类型参数实际上是一个虚拟的类型名。表示模板中出现的T是一个类型名,但是现在并未指定它到底是哪一种具体的类型。

示例:

#include<iostream>
using namespace std;
template <typename T>                   //模板声明,其中,T为类型参数
T max(T a,T b,T c)
{
    if(b>a)a=b;
    if(c>a)a=c;
    return a;
}
int main()
{
    int i1=8,i2=5,i3=6,i;
    double d1=56,d2=90.765,d3=43.1,d;
    long g1=67890,g2=-456,g3=78123,g;
    i=max(i1,i2,i3);
    g=max(g1,g2,g3);
    d=max(d1,d2,d3);
    cout<<"i_max="<<i<<endl;
    cout<<"i_max="<<d<<endl;
    cout<<"i_max="<<g<<endl;
    return 0;
}

9.2 类模板

函数模板对于功能相同而数据类型不同的一些函数,不必一一定义各个函数,可以定义一个对任何类型变量进行操作的函数模板。在调用函数时系统会根据实参的类型,取代函数模板中的类型参数,得到具体的函数。

对于类的声明来说也有同样的问题,如

class Compare_int
{
    public:
    Compare_int(int a,int b)
    {
        x=a;
        y=b;
    }
    int max()
    {
        return (x>y?x:y;)
    }
    int min()
    {
        return(x<y?x:y;)
    }
    
    Compare_float(float a, float b) {  // 构造函数参数改为float
        x = a;
        y = b;
    }
    
    float max() {  // 返回类型改为float
        return (x > y ? x : y);  // 移除多余的分号
    }
    
    float min() {  // 返回类型改为float
        return (x < y ? x : y);  // 移除多余的分号
    }
    
    private:
    int x,y;
    float x, y;
}

为减少如此繁琐的工作,可以生命一个通用的类模板,它可以由一个或多个虚拟的类型参数,如上述例子可以更改为

template<class numtype>
    class Compare
    {
        public:
        Compare(numtype a,numtype b)
        {
            x=a;
            y=b;
        }
        numtype max()
        {
            return (x>y)?x:y;
        }
        numtype min()
        {
            return (x<y)?x:y;
        }
        private:
        numtype x,y;
    }

由于类模板包含类型参数,因此又称为参数化的类。如果说类是对象的抽象,对象是类的实例。则类模板是类的抽象,类是类模板的示例。利用类模板可以建立各种数据类型的类。

类模板体中的类型并非一个实际的类型,只是一个虚拟的类型,无法用它去定义对象。必须用实际类型名去取代虚拟的类型。即在类模板名之后在尖括号内指定实际的类型名。

一般形式: 类模板名<实际类型名>对象名(参数表);

示例:声明一个类模板,利用它分别实现两个整数、浮点数和字符的比较,求出大数和小数

#include <iostream>
using namespace std;
​
// 声明类模板,虚拟类型名为 numtype
template<class numtype>  
class Compare {
public:
    // 构造函数:初始化两个比较值
    Compare(numtype a, numtype b) {  
        x = a;
        y = b;
    }
​
    // 成员函数:返回较大值
    numtype max() {  
        return (x > y ? x : y);
    }
​
    // 成员函数:返回较小值
    numtype min() {  
        return (x < y ? x : y);
    }
​
private:
    // 待实例化的类型(int/float/char 等)
    numtype x, y;  
};
​
int main() {
    // 1. 实例化 int 类型的 Compare 对象
    Compare<int> cmp1(3, 7);  
    cout << cmp1.max() << " is the Maximum of two integer numbers." << endl;
    cout << cmp1.min() << " is the Minimum of two integer numbers." << endl << endl;
​
    // 2. 实例化 float 类型的 Compare 对象
    Compare<float> cmp2(45.78, 93.6);  
    cout << cmp2.max() << " is the Maximum of two float numbers." << endl;
    cout << cmp2.min() << " is the Minimum of two float numbers." << endl << endl;
​
    // 3. 实例化 char 类型的 Compare 对象(比较 ASCII 码)
    Compare<char> cmp3('a', 'A');  
    cout << cmp3.max() << " is the Maximum of two characters." << endl;
    cout << cmp3.min() << " is the Minimum of two characters." << endl;
​
    return 0;
}

声明和使用类模板:

  1. 先写出一个实际的类。

  2. 将此类中准备改变的类型名改用一个自己制定的虚拟类型名

  3. 在类声明前加入一行,格式为 template<class 虚拟类型参数>

  4. 用类模板定义对象是用以下形式: 类模板名<实际类型名>对象名; 类模板吗<实际类型名>对象名(实参表);

  5. 如果在类模板外定义成员函数,应写成类模板形式: template<class 虚拟类型参数>

    函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表){……}

说明: (1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加 class, 如

template<class T1,class T2>
class someclass
  {…};

在定义对象时分别代入实际的类型名,如

someclass<int,double>obj;

(2) 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。如果类模板是在 A 文件开头定义的,则 A 文件范围内为有效作用域,可以在其中的任何地方使用类模板,但不能在 B 文件中用类模板定义对象。 (3) 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。关于这方面的内容不在本书中阐述,以后用到时可参阅专门的书籍或手册。


网站公告

今日签到

点亮在社区的每一天
去签到