其实这一节应该在上一次笔记的里面,但是当时还不太理解,今天回头翻看,顺手补上。
作者在书中提到了对待常量成员函数的两种流派,一种叫bitwise constness,另一种叫logical constness。
虽然看着有点蒙,但是名字还是很好的能说明些问题。bitwise认为只有当成员函数不改变任何变量的时候才是cosnt的(不改变对象的任何一个bit)
但是这样并不能保证对象内的成员就不会被改变。有个特殊情况,当成员函数返回一个引用的时候。举个例子
class Text {
private:char *m_char;
public:
Text() {
m_char = new char[30];
}
/*** this is bitwise ***/
char &operator[](size_t pos)
const{ return m_char[pos];}
};
/***main***/
int main()
{
const Text t;
t[0] = '3';
}
可以看到,虽然我把对象声明成了const,虽然我的函数是const的成员函数,但是我依然可以改变对象里面的内容!而且编译器还默许了它!
因为这样,又有一派人认为:“bitwise太虚伪了,就应该允许常量成员函数修改对象内的数据”
现在提出了一种需求,在Text中还应该加入位数校验,长度校验,这就可能会对某些成员进行更改,但我们依然想要在一个常量成员函数中完成这些事。于是他们发明的关键字 ***mutable***。再来个例子
class Text {
private:
char *m_char;
mutable size_t len;
public:
Text() {
len = 0;
m_char = new char[30];
}
/*** this is logical constness ***/
char &operator[](size_t pos)
const{
/**do something with len**/
/**like len = len + 1;**/
return m_char[pos];
}
};
看起来很棒!能在常量成员函数中更改成员了!
但是设想一下,当后续有越来越多的需求需要解决时,我们要不停的给两方加入数据变动,大量的重复.......这也太烦了,有什么简便的方法么?
答案案肯定是有,我们可以让non-const成员函数调用const成员函数。略有区别的是const的成员函数这次会返回一个const的返回值(const对象的成员函数返回值当然得是const的咯)
class Text {
private:
char *m_char;
public:
Text() {
//.........................................
}
char& Text::operator[](size_t pos)
{
return const_cast<char&>(
static_cast<const Text&>(*this)[pos]);
}
};
在这里,为了能达到让非常量函数调用常量函数的目的,得把这个类先转换成常量(不然他会一直无限循环调用本身),然后再把返回的常量转化为非常量。于是就出现了这一个丑到不行的巨大式子。
这里我有个疑惑(可能是我个人概念没搞清楚吧hh),为什么在转化对象为常量时要化为const Text&
?
然后我百度了一下才发现...原来*this返回的是个reference...干
这个item其实很好理解
太长不看版本:如果你用基类声明子类,而基类构析函数不为virtual,可能会导致内存泄漏。
先从虚表说起,在c++中只要类中存在virtual函数,那么它就会包含一张虚表,在虚表里记录了函数的地址,用来在运行时确定到底应该调用哪个函数。关于虚表讲的文章非常多,我就不展开了。
那么如果你给基类的构析函数加上了virtual,那么它在运行时就会调用正确的构析函数 举个例子
class a {
public: int *ab;
a() { ab = new int(10); }
~a() {
delete ab;
std::cout << "use a's destuctor\n";
}
};
class b : public a {
public: int *cd;
b() : a() {
cd = new int(20);
}
~b() {
std::cout << "use b's destructor\n";
delete cd;
}
};
int main()
{
a aa = new b();
delete aa;
}
简单说一下,我写了两个类,a与b,a为b的基类,接着我用a声明了一个b,再释放掉这些空间。看看运行结果。
use a's destuctor
毫无疑问是调用了a的构析函数,那这样也就意味着只释放掉了a中的int* ab的空间,而b中int* cd的空间没有正确释放,造成了内存泄漏。
那如果我们给基类a的构析函数加上virtual呢
class a {
public: int *ab;
a() { ab = new int(10); }
//changeing a's destructor to a virtual function!
virtual ~a() {
delete ab;
std::cout << "use a's destuctor\n";
}
};
结果>>
use b's destructor
use a's destuctor
可以看到现在空间被正确释放了,先调用了b的构析函数,再调用了a的构析函数。
虽然给构析函数加上virtual确实很好用,但是也会导致对象的体积增加,所以也不能乱加。
还有,如果当一个类中没有纯虚函数但你想让它成为一个抽象类,不能被实例化,怎么办呢?
可以给构析函数声明为纯虚,并且还得给它一份定义
class a
{
public: virtual~a() = 0;
};
a::~a(){}
众所周知编译器调用构析函数的顺序是先从最最深处的派生类开始,最后到基类。
而如果没有基类构析函数的定义,那么链接器就会送给你一个连接错误(喜闻乐见link2019/undefined reference
C++不禁止但是不鼓励在构析函数中抛出异常。举个例子
class widget{
public:
.....
~widget(){}
void dosomething(){
std::vector<widget> v;
...
}
};
在正常情况下,当函数dosomething执行完以后,v将自动销毁(显而易见,这会调用其中每一个元素的构析函数)。但如果在vector v被销毁时,其中的第一个widget元素抛出异常,那其余的元素该怎么办?销毁还是不销毁(导致内存泄漏哦),这是个问题。那假如第二个widget也抛出了异常,那么就同时存在两个异常了!在这种情况下,程序若是不结束运行,那么就是未定义行为
为了避免这种情况,我们可以选择在接收到异常的时候直接终止程序运行(std:abort();)也可以把选择权交给使用者。比如说,可以在构析函数之前提供一个“自制版的构析函数”,让使用者拥有处理的机会。
太长不看版:在基类构造构析期间virtual函数是基类的版本而不是派生类的版本
在构造函数中调用虚函数会带来意料之外的结果。举个小例子
class a {
public:
virtual void doSomething() = 0;
a() { doSomething(); }
};
void a::doSomething() { std::cout << "haha it's a\n"; }
class b : public a {
public:
b() : a() {}
void doSomething() override { std::cout << "haha it's b\n"; }
};
就在我打出这串代码的时候我的clangd已经贴心的给我跳了warn(仿佛在嘲笑我的愚蠢........)至于这个warn我先按下不表。
这几行代码很简单,大意就是在基类a中调用了一个纯虚函数,但在这里我做了点手脚,把这个纯虚函数给定义了。现在该谈谈main函数了
int main() {
a *a1 = new b();
delete a1;
}
好吧这边又给我报了第二个warn嘲笑我忘记了上一条item:给基类的构析函数加virtual,但我们的重点不是这个!
现在来看,这行代码执行后,会调用得是哪个doSomething呢,a的还是b的,润一下看看
结果>>
haha it's a
答案:调用的是a的,这个答案在某些程度上不令人意外,但是万一我没给那个纯虚函数写定义那么出来的可就是undefined reference了,程序会直接退出,足以说明问题的严重!
而之前我所述报的第一个warn内容如下all to pure virtual member function 'doSomething' has undefined behavior; overrides of 'doSomething' in subclasses are not available in the constructor of 'a'
大意是:在子类中重写的函数在父类中无效!而且在构造构析函数中调用纯虚函数会引发未定义行为!
这意味着如果你的本意是调用派生类的函数doSomething,那就大错特错了,你会一直纠结在为什么调用了错误版本的函数。
为了避免这种情况,你可以选择在向父类的构造函数传递足够的信息,并且把共同的部分抽象出来。
太长不看版:让赋值操作符返回*this的引用。
无了,这条太简单了。
在把自己赋值给自己时可能发生一些问题,比如提前释放掉了还在用的资源。举个例子:
class a
{
public: int *something;
...........
a& operator=(const a& rhs)
{
delete something;//释放掉自身的资源
something = new int(*rhs.something);//复制一份值
return *this;
}
}
这段程序的问题在于,如果自己给自己赋值,那么就会把自己的资源释放掉!然后把自己的指针指向一个被删除的对象!
为了防止这种问题,可以做一个简单的自我认同测试
a& operator=(const a& rhs)
{
//自我认同!
if(this == &rhs) return *this;
delete something;//释放掉自身的资源
something = new int(*rhs.something);//复制一份值
return *this;
}
还有一种方法是在复制之前先别释放掉那片空间。
a& operator=(const a& rhs)
{
//做个备份
int* tmp = something;
something = new int(*rhs.something);//复制一份值
delete tmp;//释放掉自身的资源
return *this;
}
一种方法是使用copy and swap技术
void swap(a& tmp){...}//交换 *this 和tmp的数据
a& operator=(const a& rhs)
{
a tmp(this); //调用复制函数
sawp(tmp)//交换tmp和*this的值
return *this;
} //tmp的值在执行完自动释放
由此也引申出来一种方法
a& operator=(a rhs)//pass by value,传值就是复制
{
sawp(rhs)//把rhs的复制品与*this交换
return *this;
} //tmp的值在执行完自动释放
这种方法得益于这个操作符是传值不是传址,这代表着在函数里的只是rhs的一份复制品,你可以随意操作!
如标题所写,自己写copy函数的时候把每一个成分都复制上就行啦,添加其他内容后也记得要加。
所以这章就完了?不,有一种情况很特殊。
class a
{
public: a(const a& rhs)//copy数据
{
........
}
a& operator=(const a& rhs)//copy数据
{
.......
}
private:int private_data;
};
到现在为止一切都很好,但如果我把a作为基类呢?
class b:public a
{
public: int some_data;
public: b(const b& rhs)//copy数据
:some_data(rhs.some_data)
{}
b& operator=(const b& rhs)//copy数据
{
some_data = rhs.some_data;
}
};
到现在为止一切也都正常.....吗?我们是不是忘记处理基类的数据复制了?但是基类的数据是私有的,我们够不着啊
不要慌,我们还有最后的手段,调用基类的复制函数
class b:public a
{
public: int some_data;
public: b(const b& rhs)//copy数据
:a(rhs),//调用基类的复制函数!
some_data(rhs.some_data)
{}
b& operator=(const b& rhs)//copy数据
{
a::operator=(rhs);//调用基类的复制函数!
some_data = rhs.some_data;
}
};
可能有人会发现,复制函数和复制操作符实现的内容好像很相近,那我们能不能用一个去调用另一个呢?在你问之前,不,没有语法去支持它!
其实按照我的懒惰程度,这篇文章本不应该出现的这么早,但今天很特殊,浦东封区后的第一个雨天。
自从楼道里出了个阳性,一天一抗原两天一核酸,除了核酸和扔垃圾,我就没有下过楼,我只能盯着窗户看。窗外下着雨,雨水打在玻璃上,雨滴汇成了一条线蜿蜒而下,汇集在排水槽上,流到楼下。路上几乎没有人,只有一个穿着防护服的志愿者,在运物资,他打着伞,慢慢的走出我的视线,于是街上一个人也没有了。我没见过这么冷清的雨天,孤寂的好像世界末日。
雨总是能让我想到泥土,湿滑的路面和松动的街砖,这让我蠢蠢欲动,只待雨过天晴便能出门。这时候的空气里有股清香,那是被雨水浸湿的草香,路面上落满了被雨冲下来的香樟果,黑溜溜一片,经受着腐烂,雨水在地上成了小条的河流,带出些黑色的汁水。街上的人们被雨水冲散尚未聚拢,为了跟上人群,他们的步伐加快,不自觉的踏入地面上的小水坑,鞋子带起地上的水,他们仍然义无反顾,大步向前。有的人还不知道雨停了,他们面无表情,直视前方,于是手上还撑着伞,或者是身上还披着雨衣,仿佛有一道咒语,横亘在他们与世界之间,让他们的时间永远停在了下雨的时候,让他们的路一直延伸,直到地平线之下。
然而此时我什么都做不了,我盯着窗外看,窗外没有我所说的那些景象,只有疫情下的上海。我只好回到书桌边,找点事做。