类和对象进阶
目录
构造函数&复制构造函数&析构函数
⚠️ 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
// 不调用带参数的构造函数,不输出
破案:编译器优化~
🤨比较奇怪的现象:(有点类似于复制省略)



类型转换构造函数
只有一个参数,参数类型可为多样,能够起到强制类型转换的作用(把数字隐式转换为一个临时对象)
原因如下:
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; }
强制复制消除,不调用复制构造函数(优化掉)
复制构造函数
属于构造函数的一种,也称为拷贝构造函数,只有一个参数,参数类型为本类的引用
- 不编写则编译器会自动生成-默认复制构造函数。(如果自己编写了,默认复制构造函数就不存在了,而且可以发现自己写的不一定要“复制“)
范式
// 第一种
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;
}
// 此处临时对象在经过现代编译器优化后可能结果有所不同--拷贝消除(但我们默认会调用复制构造函数)
编译器优化不大同于移动构造函数和复制构造函数
析构函数
函数名即
~类名一个类最多一个
默认自动生成,定义后则不自动生成(缺省析构函数什么也不做,最多确认基类的析构函数被调用)
对象消亡时自动被调用
⚠️动态分配的构造函数要手动写析构函数
范式
~类名(){}
或
~类名(); //声明
类名::~类名(){} //定义
作用情况
delete 触发消亡→析构
作用范围结束触发消亡→析构
(如:main 函数结束变量消亡、局部范围(如函数和{})结束局部变量消亡)
作为函数形参→函数结尾消亡
(注:引用和指针形参不属于对象,消亡对实参对象无影响)
void foo(MyClass obj) { } // obj 是独立对象,函数结束 obj 析构 void bar(MyClass& ref) { } // ref 只是实参的别名,函数结束"别名"消失 void baz(MyClass* ptr) { } // ptr 存的是地址,函数结束"地址变量"消失作为函数返回值→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++ 的类比
一些神奇的思考
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(参数表),...
{
...
}
初始化列表中的成员变量,既可以是成员对象,也可以是基本类型的成员变量
对于成员对象,“参数表”中放的就是成员对象构造函数的参数
对于基本类型的成员变量,“参数表”中即初始值
关键是要让编译器弄明白成员对象如何初始化,否则编译出错
⚠️参数可以是复杂表达式(全局函数、字符串拼接、复杂算数表达式…)
⚠️:初始化列表和{…}内赋值似乎作用相同,然而以下情况必须使用初始化列表!
const 成员**:必须在创建时初始化(因为不能赋值)
⚠️引用成员**:必须在定义时绑定对象!
🔥没有默认构造函数的类类型成员**:未初始化不可赋值!
// 参见如下示例
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 先构造 };实际构造顺序:
x先构造(类中先声明)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的友元类, 故此处可以访问其私有成员
}
};
⚠️ 你以为我是你的朋友,但我可不一定这么看

