C++类和对象详解-2
1.类的默认成员函数
1.1构造函数
class Date
{
public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全缺省构造函数
/*Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
// 编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤
Date d1; // 调⽤默认构造函数,注意为了区分函数声明,⽆参构造函数不能加括号
Date d2(2025, 1, 1); // 调⽤带参的构造函数
// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)
Date d3();
d1.Print();
d2.Print();
return 0;
}
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)//给一个缺省值4,⽤户不传参时,就默认申请4个元素的空间
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
总结·:大多数情况下,构造函数都需要自己去实现,少数情况下类似MyQueue(用栈实现队列)且Stack有默认构造时,MyQueue自动生成就可以用。
1.2析构函数
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;//调用Stack的析构
return 0;
}
假设我们自己写的析构什么都没做,无法正常完成对象中资源的清理释放⼯作时,编译器对这一类情况也进行了处理,显⽰写析构,也会⾃动调⽤Stack的析构。也就是说,对于自定义类型的成员,无论什么情况,都会自动调用它的析构函数。当然如果连Stack的析构都没有写,那就内存泄露了。
小tips:调试时若想要在类里面查看成员变量可以直接输入this。
1.3拷贝构造
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//// 编译报错:error C2652: “Date”: ⾮法的复制构造函数: 第⼀个参数不应是“Date”
//Date(Date d)//必须是引用
Date(const Date& d)//如果还有其他参数,必须写在后面并且给缺省值,比如Date(const Date& d,int x=0)
{
_year = d._year;
_month = d._month;//this->_month=d._month;
_day = d._day;
}
Date(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;
};
void Func1(Date d)//自定义类型也建议传引用,以避免传参时调用拷贝构造函数不必要的消耗
{
cout << &d << endl;
d.Print();
}
// Date Func2()
Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2024, 7, 5);
// C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉构造
// 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉
Func1(d1);
cout << &d1 << endl;
// 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
Date d3(d1);
d2.Print();
// 也可以这样写,这⾥也是拷⻉构造
Date d4 = d1;//这里特别注意一下,与后面的赋值区分等d4=d3(d4原本就存在)
d2.Print();
// Func2返回了⼀个局部对象tmp的引⽤作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
ret.Print();
return 0;
Date d1(2024, 7, 5);
Func1(d1);
}
Stack func()
{
Stack st;//调用了两次拷贝构造第一次是st拷贝给临时对象第二次是临时对象拷贝给ret(没有任何编译器优化的情况下)
return st;//返回⼀个临时对象,临时对象不受func函数销毁的影响
}
Stack& func1()
{
Stack st;
//static Stack st;ok,出了函数作用域后,st还存在
return st;//返回st的别名,但st被销毁了,拷贝失败
}//编译器报警告:返回局部变量或临时变量的地址(引用的底层是指针) st
int main()
{
Stack ret = func();
return 0;
}
//所以使用传引用返回要注意虽然能够减少拷贝,但是一定要确保在当前函数结束后还在,没有销毁才能返回
//在这个程序中可以用static修饰
直接常见的QA:
为什么传值传参会无限递归:每次调用拷贝构造函数要先传值传参,传值传参是一种拷贝,又要形成新的拷贝构造函数,要调用拷贝构造就要先传值传参......从而就形成了无穷递归。
为什么加了&之后就不会无穷递归:引用传参的意思是形参时实参的别名,没有形成新的拷贝构造。
为什么类对象的引用要加const修饰:
1.防止意外修改源对象:如“==”写成了“=”,
const确保拷贝构造函数不会意外修改被拷贝的源对象。拷贝操作应该是"只读"的,不应该改变原始对象的状态。2.允许拷贝临时对象和被声明为const对象。
为什么不能完全使用编译器⾃动⽣成的拷⻉构造:
在Stack这样的类中,_a指向了一块数组,用编译器所自动生成的拷贝构造(值拷⻉/浅拷⻉)拷贝出来的_a会指向源对象的数组,最后析构函数调用时,一块空间被析构两次,从而导致程序崩溃。不仅会导致程序崩溃,还会导致Push覆盖等等一系列问题。解决方法:用自己写的深拷贝(新对象_a先malloc源对象_a大小的空间,再把空间里的值拷贝过来,继续拷贝内置类型的成员,下面代码有进行演示)
//深拷贝操作Stack st2(st1); Stack(const Stack& st) { // 需要对_a指向资源创建同样⼤的资源再拷⻉值 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); return; } memcpy(_a, st._a, sizeof(STDataType) * st._top); _top = st._top; _capacity = st._capacity; }为什么自定义类型不建议使用传值传参:
1.性能开销:不必要的拷贝和析构
拷贝构造函数调用:传值会触发拷贝构造,如果对象包含大量数据(如动态数组、复杂嵌套结构),深拷贝可能导致显著性能损失。
析构开销:函数结束时,副本的析构函数会被调用,进一步增加开销。
2.资源管理的复杂性
深拷贝问题:如果类管理资源(如动态内存、文件句柄),错误的拷贝实现可能导致双重释放或内存泄漏。
不可拷贝类型:某些类禁用拷贝(如
std::unique_ptr),传值会导致编译错误。这个以后会了解到。3.可能引发对象切割(Slicing Problem)
仅针对继承体系:如果参数是基类类型,而传递派生类对象,传值会导致派生类独有的部分被丢弃。(同样这个以后会详细了解)
2.赋值运算符重载
2.1运算符重载
// 编译报错:“operator +”必须⾄少有⼀个类类型的形参
int operator+(int x, int y)
{
return x - y;
}
//‘.*’成员指针运算符运算符
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
//void(*)();函数指针类型
//void(A::*)();成员函数指针类型
//typedef 有两个特例函数指针类型和数组指针类型,名称要往括号里面放
typedef void(A::* PF)();
int main()
{
/*void (A::*pf)() = nullptr;*///直接声明一个成员函数指针
PF pf= &A::func;//成员函数指针类型
//注意& 普通函数函数名就是函数指针,但是成员函数指针需要加&
//(*pf)();//普通函数指针调用
A aa;//成员函数隐含this指针
//(*pf)(&aa);//不能将this指针在形参和实参中显示
(aa.*pf)();//正确的调用方式'.*'是成员函数指针调用
return 0;
}
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;
}
/*int GetYear()
{
return _year;
}*/
bool operator==(Date d2)//d1传给this,d2传给d2(参数少一个)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
//bool operator==(Date d1, Date d2)//如果和成员函数同时存在,编译器优先去类域去找,找不到再去全局找
//{
// return d1._year == d2._year
// && d1._month == d2._month//无法访问私有成员变量
// && d1._day == d2._day;
//}
// 重载为全局的⾯临对象访问私有成员变量的问题
// 有⼏种⽅法可以解决:
// 1、成员开放公有(不推荐)
// 2、Date提供getxxx函数(get是传值返回,返回的是一份拷贝,依然不能修改成员变量)
// 3、友元函数(后面会了解)
// 4、放在类里面,重载为成员函数(推荐)
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显⽰调⽤
/*operator==(d1, d2);*/
d1==d2;
// 编译器会转换成 operator==(d1, d2);左侧第一个,右侧第二个
//重载为成员函数
//编译器会转换成 d1.operator==(d2);将d1这个对象的指针传给this
d1 == d2;
return 0;
}
tips:函数重载和运算符重载没有关系,当然两个运算符重载函数,又可以构成重载函数如Date operator-(int day);和int operator-(const Date&d);(函数名相同参数不同)
2.2赋值运算符重载
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 8);
//赋值重载拷贝(对象已存在)
d1 = d2;
//拷⻉构造(创建新对象)
Date d3 = d2;
Date d4(d2);
return 0;
}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引⽤返回减少拷⻉
// d1 = d2;返回d1。
//为什么返回引⽤,因为传值返回要拷贝对象,而出了operator=函数后,d1(this)还在,为了减少拷贝,所以返回引⽤
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(d1);
Date d3(2024, 7, 6);
d1 = d3;
// 需要注意这⾥是拷⻉构造,不是赋值重载
// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;
int i, j, k;
i= j = k = 10;//连续赋值返回左操作数
d4= d1 = d2;//连续赋值
return 0;
}
一句话总结:如果要显示实现析构释放资源,就需要显示实现拷贝构造和赋值重载,反之,就不需要实现拷贝构造和赋值重载。
3.取地址运算符重载
3.1const成员函数
我们的对象有普通对象也有const对象,const对象去调用普通成员函数是有一定的限制的,如下面去调用成员函数Print(),就涉及权限放大缩小的问题
1.将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
2.正常情况下,普通对象调用普通成员函数传的this是Date*(&对象,&Date)类型,成员函数用Date* const this接受(拷贝),对指向对象的可修改性没有影响,权限平移。
3.当我们使用const对象调用普通成员函数时,会将对象的地址传给this,也就是从const Date*类型转换成Date* const this,不用管指针,而是关注this所指向的对象,也就是对象内容从不能修改变成能修改,就成了权限放大。编译器报错。这时我们要想办法用const修饰this指向的内容,但是我们又不能在实参和形参不能显示定义this,所以设计C++规定处理这种情况把,const修饰放到成员函数参数列表的后⾯。
4.const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this()
5.有些成员函数如构造函数本身就要对对象里的成员变量修改就不能加const修饰。(不修改对象的成员变量建议加const修饰)
6.本例中const成员函数的this指针是const Date* const this,第一个是我们自己加的,第二个规定,不能修改this指针的指向。
(之前我们实现日期类时,不修改都可以加如</> <=/>=....这里给一个传送门:mscode/class_date/Date.cpp · 光电笑映/c_test - 码云 - 开源中国)
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥非const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
注意:权限是否缩小,关键要看指针指向的对象(底层 const)的 const 限定是否被移除或弱化,而不是指针本身(顶层 const)的可修改性。
3.2取地址运算符重载
class Date
{
public:
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};
为什么要实现两个?因为普通对象返回Date* ,const对象返回const Date*(返回值要匹配)
int main()
{
const Date d1(2025,8,11);
Date d2(2025, 8, 12);
cout << &d1 <<endl;
cout << &d2 << endl;
return 0;
}
更多推荐


所有评论(0)