加载中

类和对象进阶

类和对象进阶

目录

构造函数&复制构造函数&析构函数

⚠️ Tips:

  • 不定义任何构造函数(包括复制构造函数)的情况下,类会自动生成

构造函数

  • 函数名与类名相同

  • 类生成必须调用未设计情况下自动生成无参构造函数否则不生成,不一定需要自己写)

  • 可以重载(即:一个类可有多个构造函数,编译器自动选择可行且唯一的)

  • 无返回值 (⚠️ void也不能写!)

  • 不分配空间,只初始化空间

  • 对象一旦生成后不能在其上执行构造函数

  • ⚠️private 的构造函数不能用于初始化

  • ⚠️无参构造函数,又称为默认构造函数(不管是自动生成的,还是人为创立的)

基本范式

class Complex{
private:
	double real,imag;
public:
    // ☀️
	Complex(double r,double i=0);
    // 若不写,则自动生成无参构造函数
    // 此时并未自动生成无参构造函数!

    Complex(Comlpex c1,Complex c2);
    //构造函数可重载,但是⚠️这个接受双参数,不是*复制*构造函数!(加了 & 也不是)
    
    void set(double r,double i){}
    //不是构造函数!
};
Complex::Complex(double r,double i){ //这里默认初始值不能再写的否则报错
 	real=r; imag=i;
    // 第二个参数默认为0
}

Complex::Complex(Complex c1,Complex c2)
{
    real=c1.real+c2.real;
    imag=c1.imag+c2.imag;
}

Complex c1; // error!
Complex *pc = new Complex; // error! 未定义也因为定义其他而不会自动生成无参构造函数
Complex c2(2); // ok
Complex c3(2,4); // ok
Complex *pc2 = new Complex(3,4) // ok
Complex c4(c2,c3); // ok!

隐式转换

在有合适的构造函数的情况下,且运行时需要就会自动进行隐式转换

Complex c1 = 7
//隐式转换 Complex(7) 时调用一次输出,复制构造函数调用(但是不输出)
// 实质:7→隐式构造Complex(7)→调用自动生成的默认复制构造函数来初始化c1

Complex c2=Comlpex(7)
//生成 Complex(7) 时调用一次普通构造函数输出,然后初始化调用复制构造函数(但是不输出)
    
// 所以两次都会 "Constuctor called",结果相同
   
Complex c2=c1
// 不调用带参数的构造函数,不输出

破案:编译器优化~

🤨比较奇怪的现象:(有点类似于复制省略)

image-2026031220410712

image-20260312205156893

image-20260312205216895

类型转换构造函数

只有一个参数,参数类型可为多样,能够起到强制类型转换的作用(把数字隐式转换为一个临时对象)

原因如下:

class Complex{
public:
	double real,imag;
	Complex(int i){...} //类型转换构造函数
	Complex(double r,double i){...}
}
int main(){
	Complex c1(7,8);
	Complex c2=12; // 是初始化语句!不是赋值!
	c1=9; // 9 将被自动创建为一个临时的 Complex 对象 Complex(9)
	cout<<c1.real<<","<<c1.imag<<endl;
	return 0;
}

🤨 Complex c2=12; // 是初始化语句!不是赋值!会调用复制构造函数吗?

可以采用explicit关键字防止隐式转换(不过vscode好像直接宰了)

class Complex{
public:
	double real,imag;
	explicit Complex(int i){...} //类型转换构造函数
	Complex(double r,double i){...}
}
int main(){
	Complex c1(7,8);
	Complex c2=Complex(12); 
	// c1=9; //error!
    	c1=Complex(9);
	cout<<c1.real<<","<<c1.imag<<endl;
	return 0;
}

image-20260312210504664

强制复制消除,不调用复制构造函数(优化掉)

复制构造函数

属于构造函数的一种,也称为拷贝构造函数只有一个参数,参数类型为本类的引用

  • 不编写则编译器会自动生成-默认复制构造函数。(如果自己编写了,默认复制构造函数就不存在了,而且可以发现自己写的不一定要“复制“)

范式

// 第一种
Complex(const Complex & a){}

// 第二种
Complex(Complex & a){}
// 或者先声明再Complex::Complex(){}亦可

注意⚠️:

  • const版本接收范围更广一些,可以接收const Complex &a 和Complex &a,而无const版本能接收后者(因可实现修改,无法保证const传入不被修改)

  • 禁止以本类的对象作为唯一参数,即

    ⚠️Complex(Complex c){} \\ ❌error! 因为调用时将生成一份,这一份又要生成一份,这一份又要生成一份…死循环

理论上,复制构造函数可以重载并调用最佳匹配,但是一般不建议
传统上:

#include <iostream>

class MyClass {
public:
 MyClass() = default;
 MyClass(MyClass&)      { std::cout << "non-const lvalue\n"; }
 MyClass(const MyClass&){ std::cout << "const lvalue\n"; }
};

int main() {
 MyClass a;
 const MyClass b;
 MyClass c = a;  // 非 const 左值 -> 调用 non-const lvalue
 MyClass d = b;  // const 左值 -> 调用 const lvalue
 MyClass e = MyClass(); // 临时对象(右值) -> 调用 const lvalue(C++98/11 都如此,编译器优化可能会产生拷贝消除而不输出)
}

输出:

non-const lvalue
const lvalue
const lvalue

此外,自己编写的复制构造函数并不一定要做复制操作,但习惯上还是保持类似于复制操作为佳

重点:被调用的三种情况

1.当用一个对象去初始化同类的另一个对象
Complex c2(c1); // copy constructor called
Complex c2=c1; // copy constructor called

c1=c2; // ❌empty output! 仅仅只是赋值,并未调用复制构造函数!

⚠️:对象间用等号赋值不导致调用复制构造函数

2.类A的对象作为某个函数的参数

函数被调用时,将调用复制构造函数

class A
{
	public:
		A(){};
    	// ☀️
		A(const A &){ cout<<"copy constructor called!"<<endl;}
    	//也可以A(const A &a){ cout<<"copy constructor called!"<<endl;}
};
void Func(A a){}
int main(){
	A a;
	Func(a);
	return 0;
}
// 会发现此处的形参不一定等于实参,取决于复制构造函数怎么写
3.类A的对象作为某个函数的返回值

函数返回时,复制构造函数被调用

class A
{
public:
	int v;
	A(int n) { v = n; };
	A(const A&a) { 
        v = a.v;
        cout << "Copy constructor called" <<endl;
	} 
};
// ☀️
A Func() { A b(4); return b; }
// 生成Func()调用复制构造函数,其实参来源于b
int main()
{ 
    cout << Func().v << endl;
    return 0; 
}
// 此处临时对象在经过现代编译器优化后可能结果有所不同--拷贝消除(但我们默认会调用复制构造函数)

编译器优化不大同于移动构造函数和复制构造函数

析构函数

  • 函数名即~类名

  • 一个类最多一个

  • 默认自动生成,定义后则不自动生成(缺省析构函数什么也不做,最多确认基类的析构函数被调用)

  • 对象消亡时自动被调用

  • ⚠️动态分配的构造函数要手动写析构函数

范式

~类名(){}

~类名(); //声明

类名::~类名(){} //定义

作用情况

  1. delete 触发消亡→析构

  2. 作用范围结束触发消亡→析构

(如:main 函数结束变量消亡、局部范围(如函数和{})结束局部变量消亡)

  1. 作为函数形参→函数结尾消亡

    (注:引用和指针形参不属于对象,消亡对实参对象无影响)

    void foo(MyClass obj) { }      // obj 是独立对象,函数结束 obj 析构
    void bar(MyClass& ref) { }     // ref 只是实参的别名,函数结束"别名"消失
    void baz(MyClass* ptr) { }     // ptr 存的是地址,函数结束"地址变量"消失
  2. 作为函数返回值→return的东西消亡

重点:生存周期

经典范例1:

class Demo {
private:
    int id;
public:
    Demo(int i) {
        id = i;
        printf("id=%d, Construct\n", id);
    }
    ~Demo() {
        printf("id=%d, Destruct\n", id);
    }
};

Demo d1(1);

void fun() {
    static Demo d2(2);
    Demo d3(3);
    printf("fun \n");
}

int main() {
    Demo d4(4);
    printf("main \n");
    {
        Demo d5(5);
    }
    fun();
    printf("endmain \n");
    return 0;
}

output:

id=1, Construct
id=4, Construct
main
id=5, Construct
id=5, Destruct
id=2, Construct
id=3, Construct
fun
id=3, Destruct
endmain
id=4, Destruct
id=2, Destruct
id=1, Destruct

经典范例2:

#include <iostream>
using namespace std;

class CMyclass {
public:
    CMyclass() {};
    CMyclass(CMyclass &c) {
        cout << "copy constructor" << endl;
    }
    ~CMyclass() {
        cout << "destructor" << endl;
    }
};

void fun(CMyclass obj_) {
    cout << "fun" << endl;
}

CMyclass c;

CMyclass Test() {
    cout << "test" << endl;
    return c;
}

int main() {
    CMyclass c1;
    fun(c1);
    Test();
    return 0;
}

output:

copy constructor
fun
destructor //参数消亡
test
copy constructor
destructor // 返回值临时对象消亡
destructor // 局部变量消亡
destructor // 全局变量消亡

静态成员(static 关键字)

⚠️ 本质上属于全局变量,写进类里是为了形式上为一个整体,易于维护和理解

  • 静态成员变量只有一份,为同类的所有对象共享⚠️
  • 静态成员变量必须在类定义外面专门定义一下T 类名::变量名(可选择进一步直接赋值)
  • 静态成员函数不具体作用在某个对象上
  • sizeof()计算不包含静态成员⚠️
  • 静态成员函数不能调用this指针(因为静态成员不具体作用于某个对象)
范式

类名::成员名 不需对象即可访问

(甚至可以在还未有对象生成时访问一个类的静态成员;普通访问方式也适用,但无意义)

其他注意点🔥🔥
static void staticFunc(Foo& obj) {
    obj.val++;        // ✅ 现在知道是哪个对象了
    obj.nonStatic();  // ✅ 可以调用
}
class Foo {
public:
    static int s_val;
    int val;

    static void staticFunc() {
        s_val++;      // ✅ 可以访问静态成员
        // val++;     // ❌ 编译错误!不知道 val 属于哪个对象
        // nonStatic(); // ❌ 同样错误
    }

    void nonStatic() {
        val++;        // ✅ 编译器隐式做了 this->val++
    }
};
  • 静态成员不具体作用于某个对象,因此:

    • 静态成员函数内部不能访问 非静态成员变量和函数 (因为它搞不清非静态成员是谁)
  • 静态成员函数的真实参数个数就是程序写的个数(普通成员函数还要加上this指针)

实战范例
#include <iostream>
using namespace std;

class CRectangle
{
private:
    int w, h;
    static int totalArea;      // 矩形总面积(静态成员变量声明)
    static int totalNumber;    // 矩形总数(静态成员变量声明)
public:
    CRectangle(int w_, int h_);     
    CRectangle(const CRectangle & other);  
    ~CRectangle();      
    static void PrintTotal();               
};

// 静态成员变量定义并初始化(必须在类外进行,否则链接错误——静态成员不类外定义→未分配内存找不到)
int CRectangle::totalNumber = 0;
int CRectangle::totalArea = 0;

CRectangle::CRectangle(int w_, int h_)
{
    w = w_;
    h = h_;
    totalNumber++;          // 新对象产生,总数加1
    totalArea += w * h;      // 新对象产生,总面积增加
}

// 复制构造函数实现(要记得!确保程序正确)
CRectangle::CRectangle(const CRectangle & other)
{
    w = other.w;
    h = other.h;
    totalNumber++;          // 复制产生新对象,总数加1
    totalArea += w * h;      // 新对象面积加入总面积
}

CRectangle::~CRectangle()
{
    totalNumber--;           // 对象消亡,总数减1
    totalArea -= w * h;      // 对象消亡,总面积减少
}

// 静态成员函数实现:输出当前总数和总面积
void CRectangle::PrintTotal()
{
    cout << totalNumber << ", " << totalArea << endl;
}

int main()
{
    CRectangle r1(3, 3), r2(2, 2);
    // cout << CRectangle::totalNumber; // 错误:totalNumber 是私有的,不能直接访问
    CRectangle::PrintTotal();  // 通过类名调用静态函数
    r1.PrintTotal();           // 通过对象调用静态函数(效果相同)
    return 0;
}

this指针

核心作用:指向成员函数所作用的对象,即指向当前对象自己的指针

非静态成员函数中可以使用 this 来代表 指向调用该成员函数的当前对象的指针)

官方的,非静态成员函数中可以使用 this 来代表 指向该函数作用的对象的指针(作用的对象其实也就是当前的对象)

最最最重要的用途:

class Complex(){
public:
	double real,imag;
	void print(){
		cout<<real<<","<<imag;
	}
	Complex(double r,double i):real(r),imag(i){}
	Complex AddOne(){
		real++; // 底层:this->real++;
		print(); // 底层:this->print();
		return *this; //⚠️⚠️⚠️操作自己的唯一方式
	}
};

C 和 C++ 的类比

image-20260312223740691

一些神奇的思考

class A {
    int i;
public:
    void Hello() { cout << "hello" << endl; }
}; // void Hello( A * this ) { cout << "hello" << endl; }

int main() {
    A * p = NULL;
    p->Hello(); // 底层转换为 Hello(p);
} //输出:hello
// 此刻不涉及指针的具体内容(Null),传入后无所谓
// 注:此做法可能会产生未定义行为
class A {
    int i;
public:
    void Hello() { cout << i << "hello" << endl; }
};
// void Hello( A * this ) { cout << this->i << "hello" << endl; }

// this若为NULL,则出错

int main() {
    A * p = NULL;
    p->Hello(); // Hello(p);
}
// 此时 this 为 Null ,但是要求访问 i ,错误!

**备注:**此处又为重要考点:即封闭类,为垒土成山的基石

封闭类

成员对象

一个类A的对象a作为另一个类B的成员变量,则a为B的成员对象

(可以说a这个B的成员变量是一个成员对象,也可以说a这个A的对象是B的成员对象)

封闭类基本定义

包含成员对象的类,即称为封闭类

经典的例子

class CTyre{
private:
	int radius;
	int width;
public:
	Ctyre(int r,int w):radius(r),width(w){}		
};

class CEngine{
};

class CCar{
private: 
    int price;
    Ctyre tyre;
    CEngine engine;
public:
    CCar(int p,int tr,int tw);
};
CCar::CCar(int p,int tr,int tw):price(p),tyre(tr,tw){
}
// CEngine 调用默认构造函数初始化
// CTyre 无默认构造函数,必须初始化列表赋值!(否则编译器不知道如何初始化)
int main(){
    CCar car(20000,17,225);
    return 0;
}

初始化列表

类名::构造函数名(‘类型 参数’表):成员变量1(参数表),成员变量2(参数表),...
{
...
}

初始化列表中的成员变量,既可以是成员对象,也可以是基本类型的成员变量

  • 对于成员对象,“参数表”中放的就是成员对象构造函数的参数

  • 对于基本类型的成员变量,“参数表”中即初始值

  • 关键是要让编译器弄明白成员对象如何初始化,否则编译出错

  • ⚠️参数可以是复杂表达式(全局函数、字符串拼接、复杂算数表达式…)

⚠️:初始化列表和{…}内赋值似乎作用相同,然而以下情况必须使用初始化列表

  1. const 成员**:必须在创建时初始化(因为不能赋值

  2. ⚠️引用成员**:必须在定义时绑定对象

  3. 🔥没有默认构造函数的类类型成员**:未初始化不可赋值!

// 参见如下示例
class Member{
public:
    Member(int x){}
};
class MyClass{
    const int c;
    int& ref;
    Member m;
    MyClass(int & r):c(100),ref(r),m(200){
        // c=100; // error! 常量不可赋值!
        // ref=&r; //error! 引用必定在初始化绑定!
        //  m=Member(200); // error! //未初始化,不可赋值!
    }
};

封闭类中构造和析构函数执行顺序

⚠️ 先拼组件,再合装为成品/先拆成组件,组件再拆成碎片

(只需记住上面👆这点即可)

构造:

  • 先执行成员对象构造函数
  • 再执行封闭类自己的构造函数

析构:

  • 先执行封闭类的析构函数

  • 然后再执行成员对象的析构函数

    ⚠️ 对象成员的构造函数调用次序取决于对象成员在类中的说明次序与在初始化列表出现次序无关

    class A {
        int x;
        int y;
    public:
        A() : y(1), x(y) { }  // 初始化列表先写 y,但 x 先构造
    };

    实际构造顺序:

    1. x 先构造(类中先声明)
    2. y 后构造(类中后声明)

    所以 x(y) 用的是未初始化的 y,结果是未定义行为

    编译器通常会给你警告(warning),但不会报错,因为语法上是合法的。C++ 标准明确规定:初始化列表的顺序不影响实际构造顺序

友元

  • 一言以蔽之,方便访问别类的私有成员(但此处不具有对称性,即你认为我是你的朋友,但我不一定
  • 友元关系不能传递不能继承!(参见继承章节)

友元函数

一个类的友元函数可以访问这个类的私有成员

// 一般的
void total_func(){}
class B{
public:
	void function();
};
class A{
    friend void total_func();
	friend void B::function(); 
};

请参看如下具体示例:

class CCar; //声明下用
// 此处因为产生了互相套用的情况,因此要提前声明,只调整顺序无效;友元关系只是声明不造成循环嵌套
class CDriver{
public:
	void ModifyCar( CCar * pCar); //改装汽车
};
class CCar{
private:
	int price;
	friend int MostExpensiveCar( CCar cars[],int total); 
	friend void CDriver::ModifyCar(CCar *pCar);
}; //// 声明友元

void CDriver::ModifyCar(CCar *pCar){
    pCar->price+=1000; // 友元成员函数可以使之改装后增值
}

int MostExpensiveCar(CCar cars[],int total){
    int tmpMax=-1;
    for(int i=0;i<total;i++){
        if(cars[i].price>tmpMax) tmpMax=cars[i].price; // 友元函数可以访问私有成员变量
    }
    return tmpMax;
}

友元类

class CCar{
private:
	int price;
	friend class CDriver; // 声明CDriver为友元类
    // ⚠️ 此处本就是声明 CDriver是一个类,故前面无需再次声明
};

class CDriver{
public:
	CCar myCar;
	void ModifyCar(){ //改装汽车
		myCar.price += 1000; 
        //因CDriver是CCar的友元类, 故此处可以访问其私有成员
	} 
};

⚠️ 你以为我是你的朋友,但我可不一定这么看

本文作者:flowwalker之码艺Blog

本文链接:/coding-notes-blog/posts/9c61/

版权声明:本文采用 CC BY-NC-SA 4.0 许可协议