这一节Java里面都有类似的概念,比较简单,就不写的太啰嗦了。
首先是认识以前面向过程的思想:
而现在再来感受一下面向对象的思想:
这玩意儿学过Java的话应该是比较简单的,就不再赘述了。
首先类中的public的内容应该放在类的最上面,这样可以让人一眼就看出该类能够给外部提供什么样的功能和作用,而private的内容则应该放在类的最底下。
其次类中的数据成员都要以_下划线开头,不过不同公司可能代码规范不太一样,选一种后面进了公司再改也是一样的。
大括号内部称为类内部,大括号之外称为类外部。
另外,class类的默认访问权限是private私有的。
类的定义过程就是类的所有成员函数实现的过程。
1、在C++中,struct的功能被扩展了,基本上与class的功能相同
2、唯一不同的是默认访问权限不同:struct是默认public的,而class默认是private的
首先要区别一下在C++中赋值与初始化的区别,这与Java是很不一样的(有点咬文嚼字的感觉,但这样会更好理解C++中的列表初始化语法),在C++中假如一个类的构造函数如下:
class Computer{
public:
Computer(int price){
_price = price;//这里在Java中其实就已经是初始化了,而在C++中严格来讲这其实只是赋值操作
}
private:
int _price;
};
int main(){
//更直观的来说
int a = 10; //这是赋值操作
int b(10); //这才是实际意义上的C++中的初始化操作
}
示例:
class Computer{
public:
//C++中初始化列表的构造器写法,也可以给予参数默认值
Computer(int price): _price(price) , _cnt(0) {}
private:
int _price;
int _cnt;
};
int x; //变量的声明,未初始化,这不推荐
int y = 0;//变量的定义,有初始化,大力提倡
int* p; //不推荐,这是野指针
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int)" << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);//存放在栈上的对象,称为栈对象
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
}
int main(){
test0();
}
编译运行:
可以看见pt对象此时作为栈上的对象确实是在被销毁时自动执行的,这就出现了析构函数的一个调用时机问题。
全局对象的销毁时机:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): " << _ix << _iy << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
//全局对象,声明周期大于main函数
//即main函数进入之前该对象就已经存在,在main函数退出之后该对象才会被销毁
Point g_pt(10,11);
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
}
int main(){
cout << "enter main()" << endl;
test0();
cout << "exit main()" << endl;
}
可以看见全局对象是在main函数进入之前就已经执行了,然后在main函数退出之后才会调用其析构函数。
静态对象的调用时机:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): " << _ix << _iy << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
//全局对象,声明周期大于main函数
//即main函数进入之前该对象就已经存在,在main函数退出之后该对象才会被销毁
//Point g_pt(10,11);
//静态对象,因为也是全局静态区,所以自然生命周期也大于main函数
static Point s_pt(22,22);
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
}
int main(){
cout << "enter main()" << endl;
test0();
cout << "exit main()" << endl;
}
编译运行:
堆对象的调用时机:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): " << _ix << _iy << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
//全局对象,声明周期大于main函数
//即main函数进入之前该对象就已经存在,在main函数退出之后该对象才会被销毁
//Point g_pt(10,11);
//静态对象,因为也是全局静态区,所以自然生命周期也大于main函数
//static Point s_pt(22,22);
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
//堆对象
Point* ppt = new Point(31,32);
cout << "*ppt: " << endl;
ppt->print();
}
int main(){
cout << "enter main()" << endl;
test0();
cout << "exit main()" << endl;
}
编译运行:
可以看见11111111部分下面的才是我们的堆对象打印的消息,可以发现只调用了构造函数却没有析构函数,这是因为我们没有手动去释放堆空间。
加上delete:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): " << _ix << _iy << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
//全局对象,声明周期大于main函数
//即main函数进入之前该对象就已经存在,在main函数退出之后该对象才会被销毁
//Point g_pt(10,11);
//静态对象,因为也是全局静态区,所以自然生命周期也大于main函数
//static Point s_pt(22,22);
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
//堆对象
Point* ppt = new Point(31,32);
cout << "*ppt: " << endl;
ppt->print();
delete ppt;
}
int main(){
cout << "enter main()" << endl;
test0();
cout << "exit main()" << endl;
}
编译运行:
可以看见此时才调用了析构函数,所以执行delete表达式时也会自动调用析构函数。
还有最后一个局部静态变量的调用时机:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): " << _ix << _iy << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
//全局对象,声明周期大于main函数
//即main函数进入之前该对象就已经存在,在main函数退出之后该对象才会被销毁
//Point g_pt(10,11);
//静态对象,因为也是全局静态区,所以自然生命周期也大于main函数
//static Point s_pt(22,22);
void test0(){
//语句块,当我们将代码包进一个语句块时
//此时它的作用域就仅剩这个范围
//当这个范围结束时即Point对象就被销毁了
{
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
}
cout << "1111111111111" << endl;
//堆对象
//Point* ppt = new Point(31,32);
//cout << "*ppt: " << endl;
//ppt->print();
//delete ppt;
//局部静态对象
//在程序中第一次调用test0函数时,会创建该局部静态对象
//之后再调用test0函数时就会直接使用而不再创建
static Point pt2(44,44);
pt2.print();
}
int main(){
cout << "enter main()" << endl;
test0();
cout << "exit main()" << endl;
}
编译运行:
可以看见局部静态变量在退出了main函数也就是整个程序执行结束之后才会调用析构函数。
对象的拷贝有两种情况:
1、对象未创建,通过拷贝的方式创建一个新对象
2、对象已经创建,要直接通过赋值语句来拷贝对象(面试常考)
下面来分情况讨论。
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int)" << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "~Point()" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
int a = 1;
int b = a; //用已经创建的对象a去初始化尚未创建的b
//同理,对于自定义类型对象Point我们也可以做到
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
cout << endl;
Point pt2 = pt;
cout << "pt2: " << endl;
pt2.print();
}
int main(){
test0();
}
编译运行:
可以看见对于自定义类型的对象我们也可以用已经创建的对象去初始化未创建的对象。
这说明系统给我们自动提供了一个拷贝构造函数,形式如下:
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int)" << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//能够使用已经创建对象去初始化尚未创建的对象的原因是
//系统给我们默认自动提供了下面这个拷贝构造函数
//这里要注意因为下面这个函数的参数正好是Point类型,就把他当作是在类内部,所以可以用.的方式访问
//但如果是其它类型的话就绝对不行
Point(const Point& rhs): _ix(rhs._ix), _iy(rhs._iy) {
cout << "拷贝对象时自动调用了 Point(const Point& rhs) 拷贝构造函数!" << endl;
}
//析构函数
~Point(){ cout << "~Point()" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
int a = 1;
int b = a; //用已经创建的对象a去初始化尚未创建的b
//同理,对于自定义类型对象Point我们也可以做到
Point pt(1,2);
cout << "pt: " << endl;
pt.print();
cout << endl;
Point pt2 = pt;
//另一种自动调用拷贝构造的形式:Point pt2(pt);
cout << "pt2: " << endl;
pt2.print();
}
int main(){
test0();
}
编译运行:
可以看见原来果然是提供了默认的拷贝构造函数。
注意:拷贝构造函数的形式是固定的,不可以改变。
去掉引用符号的感觉大致就是,当我们程序执行到Point pt2 = pt;时,因为这是一个已经创建的对象去拷贝未创建对象,所以会去调用拷贝构造函数,但是此时我们将拷贝构造函数的&符号给去掉了,在函数参数传递时本身就也是一次拷贝,即调用拷贝函数时:
const Point rhs = pt;
这很明显又是一次已经创建的对象去拷贝创建未创建对象呀,所以很自然的会再次调用拷贝构造函数,重复上述事情:
const Point rhs = pt;
这会一直持续陷入无限递归的情况,直到最后栈溢出程序崩溃结束。
先来看一段代码:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): "<< _ix << _iy << endl;
}
Point(const Point& rhs): _ix(rhs._ix), _iy(rhs._iy){
cout << "拷贝构造函数Point(const Point &)被调用啦" << endl;
}
void print(){
cout << "(" << _ix << "," << _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
Point func(){
Point pt(11,12);
pt.print();
return pt;
}
void test0(){
Point pt2 = func();
cout << "pt2: " << endl;
pt2.print();
}
int main(){
test0();
}
这一段代码肯定是没有问题的,那么现在我们去掉拷贝构造函数中的const再运行。。。
离谱的事情发生了,我看老师写的代码都是会显示报错,但是我这里没有,多方查证最后还问了GPT-4:
只能说编译器现在还挺牛逼的。
反正记住拷贝构造函数肯定别改变其形式就可以。
补充一个知识点:
int a = 1;
int& ref = a; //这是没问题的
int& ref2 = 1; //这是错误的
//这是因为上面这个1是一个字面值常量,对1是无法取地址的
&1; //错误 无法对字面值常量取地址,那么也就无法使用引用绑定,所以这种值我们也称右值(它并没有存放在内存当中)
//而a是可以去取地址的,这种我们又称为左值
//可以取地址这意味着a的数据是存放在内存当中的
&a;
所以对于这种字面值常量,我们想要用引用绑定的话,加上const关键字即可使一般引用成为常量引用即可绑定右值
const int& ref2 = 1; //正确
所以对于上面代码来说,func()函数返回的是一个临时变量,这个临时变量和1一样是个右值,是没有写进内存的跟个字面值常量性质差不多。
那么自然的执行代码 Point pt2 = func(); 在拷贝时调用拷贝函数会出现参数传递如: Point& rhs = func();
肯定会报错,因为func()返回是的个临时变量,是字面值常量,一般引用不能绑定到该值上,必须得是常量引用才行,所以为了解决这个问题我们就必须得给拷贝构造函数的参数加上const。
解释都在注释里啦:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): "<< _ix << _iy << endl;
}
Point(const Point& rhs): _ix(rhs._ix), _iy(rhs._iy){
cout << "拷贝构造函数Point(const Point &)被调用啦" << endl;
}
//每一个成员函数都拥有一个隐含的this指针
//this指针作为成员函数的第一个参数传入函数
//但这个this参数并不需要我们手动添加,编译器会自动帮我们加上
//this指针永远指向的是当前对象
//所以成员函数隐含的效果如下
void print(/* Point* const this */){
//明显this是不能更改指向的,不然咋表示对象
//this = 0x1000; 错误 this无法更改指向
cout << "(" <<this-> _ix << "," <<this-> _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
Point pt1(1,2);
cout << "pt1: ";
//编译器给我们实现的隐含效果
pt1.print(/* &pt1 */);
Point pt2(3,4);
cout << "pt2: ";
pt2.print();
}
int main(){
test0();
}
代码示例,重点看test0()函数中的部分:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): "<< _ix << _iy << endl;
}
Point(const Point& rhs): _ix(rhs._ix), _iy(rhs._iy){
cout << "拷贝构造函数Point(const Point &)被调用啦" << endl;
}
//每一个成员函数都拥有一个隐含的this指针
//this指针作为成员函数的第一个参数传入函数
//但这个this参数并不需要我们手动添加,编译器会自动帮我们加上
//this指针永远指向的是当前对象
//所以成员函数隐含的效果如下
void print(/* Point* const this */){
//明显this是不能更改指向的,不然咋表示对象
//this = 0x1000; 错误 this无法更改指向
cout << "(" <<this-> _ix << "," <<this-> _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
int a = 1, b=2;
a = b; //赋值语句
Point pt(1,2),pt2(3,4);
pt2 = pt;//赋值语句
}
int main(){
test0();
}
=就是赋值运算符,一般作用于内置类型(比如上面的int),但当我们使用=来作类与类之间的赋值时,会发现也是一样起作用的,这是因为在类中其实编译器给我们自动提供了一个赋值运算符函数:
#include
using namespace std;
//自定义类
class Point{
public:
Point(int ix = 0,int iy = 0): _ix (ix), _iy(iy){
cout << "Point(int,int): "<< _ix << _iy << endl;
}
Point(const Point& rhs): _ix(rhs._ix), _iy(rhs._iy){
cout << "拷贝构造函数Point(const Point &)被调用啦" << endl;
}
//赋值运算符函数
//不写的话系统会自动提供一个
//下面的是系统的默认实现
Point& operator=(const Point& rhs){
_ix = rhs._ix;
_iy = rhs._iy;
cout << "赋值运算符函数被运行啦" << endl;
return *this;
}
//每一个成员函数都拥有一个隐含的this指针
//this指针作为成员函数的第一个参数传入函数
//但这个this参数并不需要我们手动添加,编译器会自动帮我们加上
//this指针永远指向的是当前对象
//所以成员函数隐含的效果如下
void print(/* Point* const this */){
//明显this是不能更改指向的,不然咋表示对象
//this = 0x1000; 错误 this无法更改指向
cout << "(" <<this-> _ix << "," <<this-> _iy << ")" << endl;
}
//析构函数
~Point(){ cout << "析构函数~Point()被执行了哟" << endl;}
private:
int _ix;
int _iy;
};
void test0(){
int a = 1, b=2;
a = b; //赋值语句
Point pt(1,2),pt2(3,4);
pt2 = pt;//赋值语句,会调用赋值运算符函数,重载运算符的功能
//所以它的等价替换如下
pt2.operator=(pt);
}
int main(){
test0();
}
注意拷贝构造函数与赋值运算符函数的使用区别:
Point pt2(pt); //此时因为pt2还没有创建完成,所以肯定是在用pt来初始化pt2,自然用的是拷贝构造函数
Point pt(1,2), pt2(3,4);
pt2 = pt; //这里两个对象都已经创建完毕,所以自然是在调用赋值运算符函数来完成赋值
有些时候默认的赋值运算符函数会有隐患,比如下面涉及的情况:
简单捋一下上面的问题:
Computer是我们自己设计的类,然后其中两个数据成员一个是brand一个是price。
其中brand是char*的数据类型,这意味着它的数据是存储在堆空间上的。
当我们按照系统默认的赋值运算符函数进行两个对象之间的赋值时,就会出现上图中的两个问题:
1、c1原来申请的空间没有得到释放造成了内存泄露(因为它在赋值运算符函数中被修改了指向,此时它指向c2的堆空间地址)
2、c1和c2对象现在所指向的空间相同,导致释放时该空间会被释放两次
解决方式也很简单,重写该赋值运算符函数,改变赋值的逻辑,让逻辑如下:
先回收C1对象的堆空间,就是原来左操作数的空间,然后进行深拷贝。
如图所示:
自复制是什么意思:就是c1对象自己赋值自己,如 c1 = c1;