类和对象进阶

类和对象进阶

目录

**备注:**此处有重要考点:即构造函数、复制构造函数调用顺序(通常 print 测试)

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

⚠️ Tips:

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

构造函数

  • 函数名与类名相同

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

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

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

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

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

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

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

基本范式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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!

隐式转换

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

Complex c2=Comlpex(7)
//生成 Complex(7) 时调用一次普通构造函数输出,然后初始化调用复制构造函数(但是不输出)

// 所以两次都会 "Constuctor called",结果相同

Complex c2=c1
// 不调用带参数的构造函数,不输出

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

image-20260312204107122

image-20260312205156893

image-20260312205216895

类型转换构造函数

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

原因如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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好像直接宰了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

复制构造函数

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

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

范式

1
2
3
4
5
6
// 第一种
Complex(const Complex & a){}

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

注意⚠️:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 都如此,编译器优化可能会产生拷贝消除而不输出)
}

输出:

1
2
3
non-const lvalue
const lvalue
const lvalue

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

重点:被调用的三种情况

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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. 作为函数形参→函数结尾消亡

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

  1. 作为函数返回值→return的东西消亡

重点:生存周期

经典范例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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:

1
2
3
4
5
6
7
8
copy constructor
fun
destructor //参数消亡
test
copy constructor
destructor // 返回值临时对象消亡
destructor // 局部变量消亡
destructor // 全局变量消亡

静态成员(static 关键字)

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

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

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

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

其他注意点
  • 静态成员不具体作用于某个对象,因此:

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

实战范例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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 来代表 指向该函数作用的对象的指针(作用的对象其实也就是当前的对象)

最最最重要的用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

一些神奇的思考

1
2
3
4
5
6
7
8
9
10
11
12
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),传入后无所谓
// 注:此做法可能会产生未定义行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的成员对象)

封闭类基本定义

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

经典的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
3
4
类名::构造函数名(‘类型 参数’表):成员变量1(参数表),成员变量2(参数表),...
{
...
}

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 参见如下示例
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! //未初始化,不可赋值!
}
};

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

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

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

构造:

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

析构:

  • 先执行封闭类的析构函数
  • 然后再执行成员对象的析构函数

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

友元

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

友元函数

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

1
2
3
4
5
6
7
8
9
10
// 一般的
void total_func(){}
class B{
public:
void function();
};
class A{
friend void total_func();
friend void B::function();
};

请参看如下具体示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CCar{
private:
int price;
friend class CDriver; // 声明CDriver为友元类
// ⚠️ 此处本就是声明 CDriver是一个类,故前面无需再次声明
};

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

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