0.前言:
C++中结构体不可以有成员函数。
【结构体大小计算回忆】
在64位下,指针大小为8字节。
a1:4,
a2:4
a3:4
指针:8
sum:20 => 24,因为需要成8的倍数。【对齐数】
1. 构造函数
- 由来:C语言中struct经常忘记初始化:像栈一样给top位置和栈size
所以C++加了构造函数,会自动初始化。 - 特殊点:如果自己不手动写,编译器自己实现。对象实例化时自动调用。
- 特点:
最好写成public:类型。
此外,C++成员变量命名风格喜欢在变量前加_
1.1带缺省的构造函数
如下图,缺省和无参不可以同时存在,语法上无参和全缺省可以同时存在,因为构成了函数重载。因为如果创建Date d1;这样有二异性。它不知道调用缺省构造函数还是无参构造函数。
- 改进:
也可以只传一两个【推荐全缺省或半缺省】
1.2 默认构造函数
- 默认构造函数:特指编译器自动生成的无参构造函数,虽然编译器创建了,但是查看值,并没有被初始化。
1. 那么默认生成的是不是什么事情都没干?
答:C++里面把类型分为两类:
内置类型(基本类型如int、char、double、内置类型数组等),自定义类型【结构体、对象】。
对于自定义类型的成员变量,它编译器会自己去调用该类型的构造函数。而内置类型,就不管。所以如下year、month、day,仍然是随机值。 - 注意点:
如果你写了一个类的有参构造函数,那么系统将不提供无参构造函数,但是你最好写出来,不然如果你创建对象不给参数,就会报错。因为上面说了,系统默认给无参构造时是你自己啥构造函数都不写。为了避免错误,最好自己手动添加。**最建议你直接写一个全缺省的构造函数。**此外,如果是自定义类型有你手写的构造函数,它会自己去调用,如果你自己没写,它还会又深入去如上的A类中创建无参构造函数,但是A类中的a因为是int类型属于内置类型,又不会被初始化值。
暴露了【C++缺点】:没有对内置类型和自定义类型统一处理。
此外注意:创建对象不给参数时,直接用对象名,不加括号。
2. 析构函数:资源释放
不是完成对象的销毁,为了完成对象中的一些资源清理工作。比如:对指针在堆上申请的空间做释放。此外,一个类只有一个析构函数。
~Date()
{
//释放指针类资源,比如栈中a指针,会申请一片连续空间。;
free(a);
// 变量清0、该置0的置0
}
-
编译器生成的默认析构函数和默认构造一样,自定义的类型会去调用它本身的析构函数【就是你类中的类属性,默认析构调用它的析构】。而内置类型就不会自动删除。
-
虽然C++有缺点,但是也方便。方便就方便在类中的类属性成员变量,会自己直接调用类属性它的类的默认构造参数,直接就做完赋值了,比较方便。
总结:没有资源需要清理,不用自己实现析构函数,其中对于自定义类型会调用该自定义类型的析构,内置类型不做处理。
3. this指针
- 特点:
- this指针的类型: 类类型* const
- 只能在成员函数内部使用
- 对象中不存储this指针,this指针只是个“成员函数”的形参。当对象调用成员函数时,编译器自动把对象地址作为实参给了this形参,this形参实际是第一个成员函数的参数,它是隐藏起来的。所以对象中不存储this指针,且没必要存。
- 【注意:】可以为空,当你不输出或不使用对象成员变量时,显然可以不穿地址,所以this没必要接收。
4. 构造函数和析构函数的调用顺序:
- 最先构造的,最晚析构,结合栈特点。
所以以上选B。 - 再看一个:
解析:
- 全局的C和static D,都存在静态内存区,在这个静态区内,也得符合栈顺序。
- A是在堆上申请开,和局部静态变量是同级的,A语句早于C语句,所以A先掉构造。然后是C,再D。且A、C都在数据段,这两个析构顺序符合栈特点
最后析构顺序按照构造顺序相反。
5. 拷贝构造函数:复制对象
- 特点:
- 拷贝构造函数与构造函数构成重载。
- 使用同类型的对象值去拷贝,加const防改变且用别名&防无限递归调用。简言之,参数类型MyClass必须加&
解释2: 对象类型的参数都是临时变量,临时拷贝出来的,编译器自动调用拷贝构造函数。所以如果拷贝构造的参数是类,会无限递归。,所以一定记住:参数类型MyClass必须加&,也就是类的引用。
- 使用场景:
自定义类型用同类型对象做初始化,就是拷贝构造 - 注意:
1. 因为参数用了const&,起来别名,防止赋值写反导致先前对象被误改,所以习惯最前面加const。
2. 如果没有显示定义,系统会生成默认的拷贝构造函数。
- 内置类型成员:也会完成按字节序的拷贝。:即按一个个字节去拷贝。按字节依次去拷贝。
- 思考:
那么要不要写拷贝构造,因为发现默认生成的也很够用。是不是都不需要写?
答:不可以, 比如栈就不行,因为会做浅拷贝,栈2用栈1去拷贝复制,结果栈2析构,会把自己指向的空间释放,但是栈1析构已经释放了。所以浅拷贝导致它们指向的空间被析构两次,同一片空间,不能同时释放两次。且s2删、增,也会影响s2。所以需要深拷贝,而深拷贝需要我们自己写。
但是对于日期Date这样的类可以用。但是栈不可以用。
拷贝构造对内置类型,都只是做值拷贝或浅拷贝。有指向资源的【或说自定义类型的成员】,都不可以用系统默认生成的拷贝构造,且系统遇到了自定义类型的属性,会调用这个自定义成员属性它自己【自定义类型】的拷贝构造函数。(且被调用的类的拷贝构造函数一定是我们人为实现的,因为需要深拷贝的拷贝构造函数)。
【总结】:系统会自动生成默认拷贝构造函数,系统会对内置类型和自定义类型都会拷贝处理。但是对两者处理细节不一样,且和构造、析构都不一样。
特点:
- 构造函数,它就是为了把当前对象的值做成和参数一样的对象。所以没有返回值
- 只有一个参数。是对象的引用
- 每个类都存在 拷贝构造函数,分浅拷贝和深拷贝。默认的拷贝构造函数是浅拷贝,会拷贝参数对象中同样的地址,所以有时候可能会影响一些操作,一个对象的改变影响到这个浅拷贝的对象,比如你改变值,或析构另外一个对象。深拷贝要自己写,且有时候必须要用深拷贝。
拷贝构造使用的代码举例
// 法1
class A1(2,1);
class A2(A1);
// 法2:
class A3 = A1;
// 调用
A3 = A2; 这是在
而最下面那一行,已经有了对象了,那个涉及的是赋值,而不是拷贝构造,且对象类型的右值,编译器会自动做=运算符重载。
- 函数返回值是对象时会返回临时变量,做一次拷贝构造。
- 形参是类时。
【回忆】:
** 插入:【size_t】类型是什么**
size_t:数组下标,无符号整数类型。
无符号不能保存负数,最低存到-1。
const修饰成员函数
- 特点:
- 常函数:成员函数后面加const后称这个函数为常函数。如下:
void Person::show() const
- 常函数内不可以修改成员属性。即不可以出现类似如下:
void fun(){ this->a = val}; 这种改变成员变量值的赋值语句绝不可以。
- 成员变量声明加mutable,在常函数中依然可以修改。
- 声明和实现时,都需要加const。
【构造函数的初始化列表】
在调用构造函数之前,先会默认做初始化列表操作,这会才是在做定义。即开辟空间。而后的自动调用的构造函数,是在做初始化。
- 使用场景:
- const 成员变量,因为常量必须在定义时初始化,之后再也不能改变。
- 引用成员变量
- 自定义类型的成员变量:不使用初始化列表会调用多次构造函数。
-
注意:
类中变量声明次序就是初始化列表中的初始化顺序。 -
例题:类成员声明顺序就是构造化顺序。
因为a2声明先于a1,而a2构造列表中需要a1值,但是a1还没有,所以a2是随机值。a1是1。 -
例题:
静态变量不是常量,静态变量必须在类外初始化,而const常量才是在初始化列表,所以
【回忆】变量的声明、定义、初始化
- 声明:声明变量a,不是定义,不给a分配内存空间。如下两种算声明。一般是带着extern的都叫声明。
还有场景:函数在.h文件中做声明,而在.c文件中做定义和初始化。
extern int a; // 声明
在类中,写出类的成员变量和有哪些函数【不去实现】,也是声明。
- 定义:特指指给分配了空间。如下代码算声明a,也给a定义了。
int a;
- 初始化:给初始值。
const修饰成员变量
特点:
- 必须使用构造函数初始化列表方式做初始化,因为初始化列表处是在做定义的地方,即:在开辟空间了,而构造函数内部:在做初始化,即空间之前已经开辟好了。【要知道创建类在调用构造函数之前,先会默认做初始化列表操作,这会才是在做定义。即开辟空间】
- const成员变量需要在定义时就初始化。所以const成员变量必须在初始化列表时赋值。
const修饰对象
- const对象不能调用普通类的成员方法。
如上,对象调用成员函数,会传地址给隐藏的第一个形参this,而this的类型是:Date* const this,这代表着this指向的对象不能改变。在d1.Print()时,传&d1给this,Date传给 Date const this,没有问题,这只是保护起来了。
但是d2.Print()传&d2,它的类型是: const Date*,在const后面,即指向的内容的成员变量也不能改变。即d2对象的成员变量只读,但是传给this后,this可以改变对象成员,是权限放大了,所以不可以。**造成d2不可以调用Print(),所以这里得使用const修饰成员函数。即之前的const成员变量。但是this是隐含的,不能显示加,所以我们直接加给()的后面,这样就不能改变*this所指向的对象的成员变量。**
这里注意,在后面的const,或const一行没有,不涉及权限缩小。比如上面const Date this就是,且所有this指针,前面的const都是保护this不变,没有权限缩小。如下也是。*
const int x = a;
- 例题:
- 解析:
这是一个const成员函数,对象值不能改变,所以函数调用后,对象值无变化。
运算符重载的应用: 实现Date:
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
- 重载">“等比较运算符,这个建议从”=="开始,然后互相使用,减少代码的重复率。利用已有的重载运算符,去简写其它的。
- 重载"+“和”+=" 、 “-“和”-=”。要注意区分"+=“和”+",+和-不需要变化自己,而+=和-=需要。
+=:日期加天数:天数超这个月就一直加。月,然后月超12就加年。
且最后返回的是当前值,所以函数返回值是引用类型、返回*this。
+:本身不能加day,所以用临时变量。而**用临时变量,就别引用。**用了引用就出错了,因为那个date不存在了。
Date& Date::operator+=(int day)
{
_day += day;
int tmp = (*this).GetMonthDay(_year, _month);
while (_day > tmp)
{
_day -= tmp;
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
tmp = (*this).GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator+(int day)
{
Date d1(*this);
d1._day += day;
return d1;
}
- 重载++:分前置和后置:写法上为了区分:后置加了默认参数int,写为:operator++(int)。创始人主要是为了加个参数做区分。编译的时编译器会多传一个0。
其中:前置++,要返回自己,所以用引用类型传即可。
而后置++,应该返回自己的原来,所以用拷贝的临时变量,所以返回普通类类型。
分析:前置不使用拷贝构造函数。而后置++用两次拷贝构造,所以推荐用前置++。
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date& Date::operator++(int)
{
Date d = *this;
*this += 1;
return d;
}
-
重载"-“、”-=":
-、-=
这里调用GetMonthDay时写_year、_month就好,加*this也行,但是难看。 -
两个日期天数之差
有以下几种思路:
a. 求和公元0年第1天,再作差。
b. 让小日期加到大日期
运算符重载
- 为了增强代码可读性,让自定义类型对象支持运算符。这也是STL的关键利器,此外: “::”、“.*”、“?:”、"."不可以重载。
都是需要自己重载。 - 应用:
比如:日期对象之间做加减。 - 注意:
- 返回值类型需要自己考虑,三操作数不允许重载,一般是单、双。
- 有几个操作数,就有几个参数。(建议传引用)因为用拷贝构造,对象类型参数必须用引用。否则无限递归且加const使得更安全,如果下面写错了会报错。
- 运算符重载上要使用成员变量,而成员变量总是私有那么如何提供?
- private变public
- 造get方法
- 友元:比public好,但会稍破坏封装。
- 成员函数法:把opeartor放到类中,参数减少一个,使opeartor变为成员函数。
变成成员函数后,函数多了个 隐藏参数:this指针。
- 使用:
cout时候要注意:
cout<< d1 > d2 << endl;
这样要小心流顺序,<<优先级高于>,所以为了他俩能计算,把d1和d2括起来。 - d1>d2,编译器会自动转为:operator>(d1, d2) ,但是为什么不支持直接自写:opeartor(>d1,d2)?
答:开头说了:为了可读性。显然,直接写符号更易懂看着舒服。
:: 、sizeof、? . 这四个不要重载。 .*也不能重载。其中点用来做对象访问成员的。
C++必须写返回值类型。
全局和类上都写,会不会重定义:即非要在外面写operator以">"为例,写两个参数,不会编译报错,因为构成重载。一般建议写成员中的。类中的函数,不是属于全局,属于类域。且大部分、习惯上, 都把运算符重载放在类中,所以优先去类中找这也寻找本身更有优势。
最后发现调用成员,优先去成员中找。
赋值运算符重载:
使用场景:比如日期类Date,想让Date d1和d3相等。实现opeartor赋值。
不需要有返回值。
- 为什么使用引用类型返回就会减少拷贝构造函数的调用?
比如: d1=d2=d3,如果此时使用如下操作符重载,会调用两次拷贝构造函数。
前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身,除非用别名&引用,才返回这个东西,且不会调用拷贝构造函数
前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身
前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身Date operator=(const Date & d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
因为返回了一个Date类型对象,是临时的,它本质通过了拷贝构造。所以调用拷贝构造在返回时。
而使用引用做返回,则一次都不使用。
Date& operator=(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
当重载等号以引用返回,返回的是已经存在的对象,一直用的是等号重载。
此外,极端情况自己给自己赋值,就可以不用处理了,直接判断后跳过。
且用地址比较,不要去比较对象。
this指针是地址,&d也是地址。如果y用:*this!=d,还要对!=做重载,因为两边是两个对象, 不能直接比。
此外,不写赋值运算符重载,编译器会自己生成一个。
此外,不写赋值运算符重载,编译器会自己生成一个。
此外,不写赋值运算符重载,编译器会自己生成一个。
编译器默认生成的赋值重载,根拷贝构造做的事情类似。
- 内置类型成员,会完成字节序值拷贝——浅拷贝。
- 自定义类型成员,会调用自定义类型的=重载。
如上,如果是队列用两个栈实现,那么这个队列不需要自己实现赋值重载,因为内含Stack拥有,且它自己没有一些连续内存的地址空间。但是如果栈本身,就需要自己实现赋值重载,因为这个自定义类型中使用了指针,需要手动释放。
总结,默认生成的四个默认成员函数,构造和析构处理机制是类似的。
拷贝构造和赋值重载处理机制是基本类似的。
- 注意点:
Date d5 = d1;
这是拷贝构造,不是赋值重载。
匿名对象
比如:创建一个对象只是为了传参。
一般对象声明周期在当前函数。
匿名对象生命周期只在当前这一行。
如果直接传匿名对象,拷贝构造会少调用一次。
【取地址&运算符重载】
默认成员函数:不写,也可以对这是取到地址,所以说它是默认重载的。
赋值运算符重载
- 默认运算符“=”只是实现了"浅层复制"功能。
- 重载赋值运算符
- 赋值=运算符重载只能在类内重载
下列关于赋值运算符“=”重载的叙述中,正确的是( )
A.赋值运算符只能作为类的成员函数重载
B.默认的赋值运算符实现了“深层复制”功能
C.重载的赋值运算符函数有两个本类对象作为形参
D.如果己经定义了复制拷贝构造函数,就不能重载赋值运算符
根据上面引用,知道A对,BC错,而D:可以有拷贝构造,也可以重载赋值=运算符。
重载运算符:流插入<<和提取>>【友元中】
流重载是:双操作数的操作符重载
本质是:对象对 流对象的操作。
所以有两个操作数,必须第一个是操作数,第二个参数是右操作数。
所以不能重载成重载函数。
如果还想cout到左边,不能实现成员函数,所以就做一个全局,不是成员函数,就不区分左右了,因为成员函数,第一个必须是操作数,默认是类类型的,也就是那个对象。
【改进】:写全局运算符重载。不写类运算符重载。
- 【面试题】:
哈希冲突如何解决?
散列法、双重哈希等等。