C++ 类(中)
Posted 玄鸟轩墨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 类(中)相关的知识,希望对你有一定的参考价值。
@toc
写在前面
我们已经初步的认识到到类的内容了,但是那些都是基础的,今天我们来分享一些比较细节的内容,也是对我知识的一个梳理,难度比较大,所以这个博客我可能写的不太好,到时候有什么问题,可以直接在评论区留言,我看到立马回复,要是我也不懂的,就去给你们查资料,还请谅解.
类的6个默认成员函数
这个博客主要就是和大家分享成员函数(方法),里面的内容比较多,我一个一个来吧,我们现在来看道例题,作为今天的开篇.
class Person
;
这不就是一个空类吗?里面什么都没有.要是你这么想,就有些简单了,是的,我们看到里面什么都没有,但是实际上编译器会自动生成六个默认的成员函数,他们存在这个类里面,至于如何验证,单机先不要着急.
我们先来看看这六个成员函数,但是我们只详细说其中的四个,另外的两个不太重要,简略的谈谈就可以了.
- 构造函数 完成对象的初始化.不是构造对象
- 析构函数 完成资源的清理
- 拷贝构造 对象拷贝必须调用拷贝函数
- 赋值重载 重新定义运算符
构造函数
构造函数又称构造方法,在C++中是帮助我们实例化对象的作用,记住是帮助我实例化对象.我们先来看看这个东西.
class Person
public:
void Print()
cout << "我叫" << _name << ",今年" << _age << "岁了" << endl;
void Set(const char* name, const int age)
strcpy(_name, name);
_age = age;
private:
int _age;
char _name[20];
;
int main()
Person per1;
per1.Set("张三", 18); //每次都要 set
per1.Print();
return 0;
构造函数的特性
现在我们就可以正式认识构造函数了,构造函数存在下面的几个特性.
- 没有返回值 记住返回值是没有 不是void
- 函数名和类名一样
- 支持重载
- 对象实例化时编译器自动调用对应的构造函数
我们先来把前面的代码优化一下,写一个构造函数
class Person
public:
void Print()
cout << "我叫" << _name << ",今年" << _age << "岁了" << endl;
// 带有 两个 参数(编译器的那个 this 没算)的 构造函数
Person(const char* name, const int age)
strcpy(_name, name);
_age = age;
private:
int _age;
char _name[20];
;
int main()
Person per1("张三", 18);
per1.Print();
return 0;
默认构造函数
这个属于构造函数的特别的一类,我们需要先看看下面的东西,只是初步认识,后面细谈.
- 编译器自动生成
- 无参的构造函数
- 全缺省的构造函数
编译器自动生成
一般情况下,构造函数谈到上面那里就可以了,但是对于我们还远远不够.我们需要理解一些东西.再刚开始的代码中,我想问问类里面存在构造函数吗?这个问题,我一开始就回答了,存在的编译器自动生成一个构造函数.
我们验证一下,假如我们在类类里面自己写了构造函数,看看会什么?
class A
public:
A(int a)
;
int main()
A _a;
return 0;
我们开始疑惑,我们不是写了一个构造函数,为何会报错,.我们可以一眼看出,我们实例化对象的时候没有传参,也就是问题所在,从这里我们就可以看出,实例化对象的时候编译器会自动调用相匹配的构造函数,也就是说,实例化对象的时候定会存在构造函数的参与.
但是看看下面的代码,为何不报错? 原因就是编译器自动生成了一个无参的构造函数,至于它的结构是啥,就不需要了解了.
class B
;
int main()
B b;
return 0;
无参的构造函数
我们自己写的无参的构造函数也是属于默认的构造函数,这里和大家说一下.
这里面就比较简单了,至于代码里面的那个问题,不要急,最后我会说的.
class A
public:
A()
a = 0;
void Print()
cout << "无参构造函数" << endl;
cout << "a = " << a << endl;
public :
int a;
;
int main()
A a; // 为哈 不是 写 A a()?
a.Print();
return 0;
全缺省的构造函数
这是最后一个了,一般到这里而言我们就对默认构造函数很了解了,理解这个就比较简单了.我们看看现象基本可以了.
class A
public:
// 全缺省
A( int a = 10)
_a = a;
void Print()
cout << "无参构造函数" << endl;
cout << "a = " << _a << endl;
public :
int _a;
;
int main()
A a; // 为哈 不是 写 A a()?
a.Print();
return 0;
为哈是 Aa
这个就要来解决上面遗留的问题了,这个说实话是语法规定的,我也很难搞懂,但是我们可以通过现象来搞懂一些东西.
先解决编译器自己生成的那个,这里我们解决不了,这里现象也不看了,记住就行.
再开始解决自己写的自己写的无参的构造函数,我们看看现象.
class A
public:
A()
void Print()
cout << "你好,世界" << endl;
;
int main()
A a();
a.Print();
return 0;
最后一个就是全缺省构造函数
class A
public:
// 全缺省
A(int a = 10)
_a = a;
A()
void Print()
cout << "无参构造函数" << endl;
cout << "a = " << _a << endl;
public:
int _a;
;
int main()
A a();
a.Print();
return 0;
这个报错和上面一样,这个我们就不理解了,全缺省不是允许我们可以这么做吗?是的,但是这里就不允许了.
默认构造函数的优先级
这个我想和大家谈谈,也不知道我的标题名字写的准确不准确,我也不知道这个知识点的名字叫什么,这里先给大家描述一下,
假如 一个类里面存在无参的构造函数,也存在一个全缺省的构造函数,我们使用 A a;调用的是哪个构造方法?这里用现象来得到答案.
class A
public:
// 全缺省
A(int a = 10)
cout << "全缺省" << endl;
//无参
A()
cout << "无参" << endl;
;
int main()
A a;
return 0;
默认构造函数的作用
我们前面就提了,构造函数是为了我们进行初始化的.但是这个初始化也是有很大的问题的,这里C++在之前有很大的缺陷,直到C++11才弥补了一部分.
对内置类型不做改变
C++这个特性很让人苦恼,在使用默认构造的时候对内置类型竟然不做处理,是在是有点难以让人接受.我们一看就可以明白了.
class A
public:
A()
private:
int _a;
double _d;
;
int main()
A a;
return 0;
C++11填坑
这个坑太大了,反正我是相对这个问题骂骂咧咧,幸好C++11把这个坑给填了,但是这个方法也会出初学者造成很大的问题,我先说方法
class A
public:
A()
private:
int _a = 0; // 在这 声明
double _d = 0.0;
;
对自定义类型进行初始化
上面的标题描述的也不太准确,这句话可以这样说,对于自定类型,编译器会调用这个自定义类的==默认构造函数==,来帮助进行初始化.
class A
public:
A()
cout << "辅助 进行 初始化" << endl;
private:
int _a;
double _d;
;
class B
public:
B()
A _aa;
;
int main()
B b;
return 0;
我们需要看看里面的结果,再来一次调试.
有人可能眼见,看到_aa里面的内容也没有进行初始化,这是由于我们在A类中没有把默认初始化给写好,我重新写一下A类,在调试一下.这样就可以了.
class A
public:
A()
//在这写好
_a = 0;
_d = 0.0;
private:
int _a;
double _d;
;
总结
说了这么多,现在需要来个总结,我们学习了构造函数,知道了默认构造函数,也明白了构造函数的作用,这些都是比较有难度的.
析构函数
如果说构造函数是为了初始化,那么析构函数就是为了资源的清理工作,对于一些比较用以忘得程序员,这是一个福音,比如我们使用malloc开辟了一款空间,有的时候容易忘记free掉,这就会造成内存泄漏.这就会有一定得问题.但是析构函数可以在对象生命周期结束后,会自动调用这个析构函数.我们只需要在这析构函数free里面就可以了.
析构函数得特性
我们先来看看析构函数得特性,这是我们得基础.
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
编译器自动调用
我们先看看个例子.
class A
public:
A(int cap = 4)
int* arr = (int*)malloc(sizeof(int) * cap);
assert(arr);
a = arr;
memset(arr, 0, sizeof(int) * cap);
_cap = cap;
~A()
_cap = 0;
free(a);
a = nullptr;
private:
int* a;
int _cap;
;
int main()
A a;
return 0;
从上面得动图我们就可以知道了,在对象a得声明周期结束后,编译器会自动调用析构函数,完成资源得清理.
默认生成得析构函数
我们需要看看默认生成得析构函数会怎样,这样可以帮助我们更高效得写出代码.
析构函数会对内置类型进行资源清理吗
很抱歉,并不能帮助我们把内置类型给清理掉.
class A
public:
A(int cap = 4)
int* arr = (int*)malloc(sizeof(int) * cap);
assert(arr);
a = arr;
memset(arr, 0, sizeof(int) * cap);
_cap = cap;
private:
int* a;
int _cap;
;
析构函数可以清理自定义类型吗
这个是可以得,不过需要调用自定义类型得析构函数,这个和默认构造函数的初始化一样的.
会调用自定义了类型的析构函数
class A
public:
~A()
cout << "自定义类型的析构函数" << endl;
;
class B
private:
A _aa;
;
int main()
B b;
return 0;
我们也可以看看是如何调用的析构函数.
class A
public:
A(int cap = 4)
int* arr = (int*)malloc(sizeof(int) * cap);
assert(arr);
a = arr;
memset(arr, 0, sizeof(int) * cap);
_cap = cap;
~A()
_cap = 0;
free(a);
a = nullptr;
private:
int* a;
int _cap;
;
class B
public:
B()
private:
A _aa;
;
int main()
B b;
return 0;
总结
这样我们也可以得到一个结果,对于自定类型我们不需要写析构函数,对于内置类型需要进行资源清理,避免内存泄漏.
拷贝构造
那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?拷贝构造是构造函数的一种,也是我们未来写类比较关键的内容,我们需要了解一下。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象
创建新对象时由编译器自动调用
值拷贝
我们起初在学习函数的时候,大多时候都会给函数传入参数,也就是编译器另开辟一块空间,把要出传入的内容拷贝一份放到这块空间里面.这就是简单的值拷贝.
我们确实需要好好看看这个值拷贝,我们发现它们的地址是不一样的.
void func(int b)
cout << "&a" << &b << endl;
int main()
int a = 10;
func(a);
cout <<"&a" << &a << endl;
return 0;
对于一些简单的类型,这个拷贝是没有问题的,但是现在我要和大家看看这个.
void func(int* pb)
free(pb);
int main()
int* arr = nullptr;
arr = (int*)malloc(sizeof(int) * 4);
func(arr);
free(arr);
return 0;
我们就会发现一个问题,对于一些类型,简单的值拷贝完全不够,上面为何会报错?原因就是我们把数组名作为参数,编译器简单的把它当做了一个指针,拷贝给了pb,但是pb的所指向的内容是没有变的,所以我们free掉了两次,程序会中断.
拷贝构造的特性
我们认识到了值拷贝,现在我们就可以说拷贝构造了,拷贝构造也是编译器默认生成的构造函数,函数名和类名一样.
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
第二个话题我们先不谈,最后分享.
默认生成的拷贝构造
我们先来看看默认生成的拷贝构造作用如何,来看看我们何时需要自己写拷贝构造
class A
public:
A(int a = 0,double d = 0.0)
_a = a;
_d = d;
~A()
_a = 0;
_d = 0.0;
public:
int _a;
double _d;
;
int main()
A _aa(1, 3.0);
cout<< "_aa._a = " << _aa._a;
cout<< " _aa._d = " << _aa._d << endl;
A _bb(_aa);
cout << "_bb._a = " << _bb._a;
cout << " _bb._d = " << _bb._d << endl;
return 0;
这个构造可以可以说是一样,所以我们不用担心编译器这一次不会不管内置类型了,这一点很好,但是也出现问题了,下面来看.
默认构造函数是值拷贝吗
这个问题很严重,要知道我们对象在生命周期结束后是会调用析构函数的,要是出现两次free这个情况,我想谁都会骂娘.
看看吧
class A
public:
A(int cap = 4)
int* pa = (int*)malloc(sizeof(int) * cap);
assert(pa);
_array = pa;
~A()
free(_array);
_array = nullptr;
public:
int* _array;
;
void func(A _bb)
cout << _bb._array << endl;
int main()
A _aa;
func(_aa);
cout << _aa._array << endl;
return 0;
int main()
A _aa;
A _bb(_aa);
return 0;
手写构造函数
分享了这么多,我们好象还没有手写构造函数,这个来个普通的,但是里面的细节也很多.
class A
public:
A(int a = 0, double d = 0.0)
_a = a;
_d = d;
A(const A& a)
_a = a._a;
_d = a._d;
private:
int _a;
double _d;
;
我们开始抠细节了.
为何用 const 修饰
很好,我们可以不用const修饰,但是有时候会写出这样的代码.
A(const A& a)
a._a = _a; //写反了
_d = a._d;
用const修饰就可以避免这种失误,因为它编译不过,可以很快的查出问题所在.
为何使用引用
你发现了最为重要的东西,首先要记住一点,<font color = red>自定义类型要实现拷贝,必须先调用构造函数</font>,如果你写的是普通传参,那也要进行拷贝,需要构造函数,编译器开始寻找构造函数,找到构造函数发现要进行拷贝,寻找构造函数.....出现死循环,所以我们要使用别名,避免拷贝.
构造函数对内置类型怎么办
这个我们前面已经说的很详细了,这里就给出一个结论,如果你的类里面没有指向同一片空间的这种类似的属性,用默认的就可以了,但是要是存在,就需要自己来写,至于如何写这涉及到深浅拷贝的知识了,这里就不谈了.这个规律适合大部分情况.
构造函数对自定义类型怎么办
这个我就不放动图了,它和构造函数以及析构函数一样,去寻找自定义类型自己的构造函数.
赋值运算符重载
本来想分两篇来写的,这个也是一个很大的内容,我们可以自己创造赋值运算符的实现规则,我们先来看看什么是赋值运算符重载.
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
为何出现赋值运算符重载
我们先来看一种请况.
为啥会报错,我就想让他们比较一下,我有什么错?可编译器却不允许,今天我必须让它给我允许了,这就是赋值运算符重载 为何会出现的原因
class Date
public:
Date(int year = 1900, int month = 1, int day = 1)
_year = year;
_month = month;
_day = day;
private:
int _year;
int _month;
int _day;
;
int main()
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (d1 == d2)
cout << "==" << endl;
return 0;
赋值运算符重载
多的不说,我们现在就来看看如何使它变得合理.
我们来看看这个函数,现在出现了一个问题,我们得不到类的属性,它被封装了,我们先把属性改成public,后面在解决这个问题.
bool operator==(Date d1, Date d2)
//在这里 年月日都相等 才是 相等
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
这里就可以了,我们调用一下这个函数
int main()
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (operator==(d1,d2))
cout << "==" << endl;
return 0;
我们可能会疑惑,我随便取一个函数名就可以把这个函数的功能给写出来,还用弄得这样花里胡哨,但是你写的函数可以被这样调用吗?但是我的就可以.
if (d1 == d2)
cout << "==" << endl;
这个就是运算符重载的魅力,现在我需要把这个函数给完善下,传引用,没必要在开辟空间了,用const修饰,避免被不小心修改
bool operator==(const Date& d1, const Date& d2)
//在这里 年月日都相等 才是 相等
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
解决不能得到属性的问题
这个我给两个解决方法,一个是在类里面写一些get函数,得到这些属性的值,另一个是使用友元,但是这种方法破坏了封装,不太建议.
在类里面写运算符重载
我们还不如直接在类里面写这个函数呢,简单快捷,这样就可以避免破坏封装.
class Date
public:
Date(int year = 1900, int month = 1, int day = 1)
_year = year;
_month = month;
_day = day;
bool operator==(const Date& d1, const Date& d2)
//在这里 年月日都相等 才是 相等
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
public:
int _year;
int _month;
int _day;
;
bool operator==(const Date& d) //默认添加一个 this 指针
//在这里 年月日都相等 才是 相等
return _year == d._year
&& _month == d._month
&& _day == d._day;
这样函数的调用就变成这样
int main()
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (d1 == d2) // d1 == d2 默认 变成 d1.operator==(d2)
cout << "==" << endl;
return 0;
class Date
public:
Date(int year = 1900, int month = 1, int day = 1)
_year = year;
_month = month;
_day = day;
bool operator==(const Date& d)
cout << "this" << this << endl;
return true;
public:
int _year;
int _month;
int _day;
;
int main()
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
cout << "d1" << &d1 << endl;
cout << "d2" << &d2 << endl;
if (d1 == d2)
return 0;
重载 运算符 "="
本来我想和大家分享一个日期类,但是如果现在这这里写了,至少还需要5000字,我把它单独放到了一个博客,作为我们这些天学习类一个小总结,在这个日期类里面,你会发现我们上面谈的所有的知识点,这里先来点小菜,这个很简单,目的是为了引出下面的知识点.
Date& operator=(const Date& d)
if (this != &d)
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
我们先来调用一下.
int main()
Date d1(2022, 10, 18);
Date d2;
d2 = d1;
d2.Print();
return 0;
d2 = d1
细心的朋友可能发现我们写的是d2 = d1;而不是在开辟d2的时候给他赋值,这里我要重点谈下.我们通过调试来看看吧.
这个调用的是运算符重载.
int main()
Date d1(2022, 10, 18);
Date d2;
d2 = d1;
d2.Print();
return 0;
Date d2 = d1
这个调用的是拷贝构造,不是那个运算符重载.
int main()
Date d1(2022, 10, 18);
Date d2 = d1;
d2.Print();
return 0;
以上是关于C++ 类(中)的主要内容,如果未能解决你的问题,请参考以下文章