C++线下测试回顾

题目地址

有同学说不知道怎么画内存模型图,我这里附几个教程
UML类图小结
UML类图与类的关系详解
类似的教程笔记网上有不少的,大家自己搜来看看把习题的类图实现一下,鉴于文章篇幅我这里就不做画图教程了。

首先我们要明确下规范,写函数内容的时候,最好使用this指针

this->width = width;
this->height = height;

而非:

width = width;
height = height;

为什么要这样写?这样写的好处是什么呢?

我们应该知道当局部变量名与全局变量同名时,全局变量会被覆盖,我们自己写构造函数的时候很可能会选择用不同名的变量名来进行赋值操作,像这样的:

width_d = width;
height_d = height;

但最好是写成这样的:

this->width = width;
this->height = height;

这里的this非常明确地指出了左边的width是当前类的成员,而不至于令别人看的时候摸不着头脑,不清楚这里的width到底哪里的东西。

我最后是这样写的:

this->width = width?width:0;
this->height = height?height:0;

附带对width,height的判断检查。

从简单的构造函数谈起

刚拿到题的时候我是呵呵笑的,脑袋中已经有了初始模型,直到我下笔写的时候……

说来惭愧,我写第一个构造函数的就卡住了,磨磨蹭蹭十来分钟过去了还是想不出怎么调用Point类里的数据成员(x, y)比较好。

原题中Rectangle类定义了一个指向Point的指针leftUp。我们可以利用这个指针来对(x, y)进行构造赋值。

起初我是这样写的:

x = leftUp->x;
y = leftUp->y;

……编译器啪啪啪打脸,简直不忍直视,大家帮忙分析下这样赋值错在哪里?

左边的x,y到底是谁?代表的是Point的成员还是另外创建的数据?leftUp之前只是定义了下,这个指针并未在堆中被创建,leftUp还没有被实例化哪里来的成员(x,y)?

很多时候就是这样的,感觉自己挺懂的,下笔写出来的往编译器来一丢立马报一堆错。

你说我没有实例化,那么我来搞一个

this->leftUp = Point* ptr;
this->leftUp->x = ptr->x;
this->leftUp->y = ptr->y;

这样乍看之下似乎无错,但大多数负责任的编译器仍会报错,这是为什么呢?

这里涉及到设计C++类、函数要考虑到的一些东西。当我们编写一个函数的时候通常会默认我们调用的外部的数据是安全的,是可用的,同时为了保证自己的鲁棒性我们要防止外部改动导致本函数的失效或者更为恶劣的程序崩溃。所以我们在对指针指向的类成员变量赋值时最好是这样做:

this->leftUp = new Point(x, y);

直接在堆中new一个,同时调用Point的默认构造函数传进(x, y)值;

你真的会拷贝构造吗?

老师讲完拷贝构造的时候我问了老刁,你拷贝类Shape里的no了吗?我们开始都没注意到这个值,不过细心的网友应该记着Rectangle是继承Shape而来的。所以他默认的数据里还是有no这个成员的,你怎么能抛弃他呢?但是我们要怎么给他拷贝复制呢?

我们使用Shape(other),直接来调用父类Shape的默认构造函数。为啥可以这样写呢?直接调用父类不会出错吗?

不会的,如果你认真看了侯捷老师的视频,应该已经知道子、父类之间会为友元。

完整的代码如下


inline
Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)
{
    if(other.leftUp != nullptr)
    {
        this->leftUp = new Point(*other.leftUp);
    }
    else
    {
        this->leftUp = nullptr;
    }
}

有的人可能会将函数名后紧跟的初始化操作写成这样的顺序:

width(other.width), height(other.height), Shape(other)

这时候李老师问了一个问题:你认为拷贝构造时的顺序是怎样的?是按照你写的初始化的顺序吗?

几乎所有人都回答“是”。

然而事实是有点坑爹的,构造函数开头的

inline
Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)

并不是按照你写的顺序来的,而是按照编译器定义的优先级来的,先是拷贝构造父类的数据,然后是原类里对数据定义的顺序,所以你在开头考虑写成什么顺序并没有什么卵用,无论你写成什么顺序,他内部都已经有约定好的顺序了,但是为了代码阅读方便,让人一看便知拷贝构造的顺序,你这里只要按照他内部约定好的顺序来写,先父类,后定义顺序,权当做个顺序说明好了。

最后我们要面对的是leftUp这个指针成员,很多人可能会直接这样写:

this->leftUp = new Point(*other.leftUp);

如果你拷贝的other里的leftUp是个空指针呢?我们还在堆里创建他干嘛?
所以这里要加个if判断。

if(other.leftUp != nullptr)
{
    this->leftUp = new Point(*other.leftUp);
}
else
{
    this->leftUp = nullptr;
}

为什么我用的是nullptr而不是NULL,null或者0?你可以参考下我下面给的连接,相信你看完会总结出一个属于自己的答案。

NULL,0,ptrnull全解析
C/C++ 中 0 与 NULL 区别是什么?
NULL VS ptrnull

赋值操作符-你不造的那些事儿

只有构造函数可以这样

Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)
{
    ···
}

在花括号之前这样直接初始化,赋值操作符是不可以的,这是构造函数才拥有的特例。

所以我们还是老老实实用this指针吧。不过我相信很多人会忘了在开头写这句判断:

if(this == &other)
{
    return *this;
}

如果他赋值操作的就是他本身,我们不加判断的直接进行操作,在处理leftUp的时候,

this->leftUp = new Point(*other.leftUp);

已有的other.leftUp被再次指向了一个新的Point对象,但你原来的的other.left并没有被销毁,原来的数据不再被记录在案,换句话说你搞丢他了,这样便出现了内存泄露。

不过不用惊讶,李老师说“大部分程序员的c++程序里必然会出现内存泄露的问题”,所以要想成为那少数的“大牛”,就从现在开始养成良好的习惯,学习画你的数据内存模型图,搞清楚每一个数据的动向、联系。确保你写的程序万无一失。

接下来我们要思考父类Shape要怎么写呢?

说实话,我对处理继承的父类的东西是一窍不通的,连上面那个构造函数Shape(other)都是看的别人的,看到这个直接歇菜了。我开始写了个这样的

Shape(other.no);

连我自己都不知道这是什么鬼,一提笔便暴漏出很多问题来。我们几个C++都不晓得该怎么处理,然后有人从网上找了个答案。写成了酱紫:

Shape::operator=(other);

李老师后来过来看到我们这样写还以为我们是懂的,说这是对父类操作符重载的标准写法。

这里我们把“operator=”看做一个整体,即Shape的成员函数,然后我们直接传入参数other,这样便调用了shape的默认构造函数,对no进行的赋值操作,这样做的好处是我们完全不必管Shape内部是如何实现的,以及是否发生改动,我们只管做我们的赋值操作就OK了。

下面我要给大家当下反面教材。当时我问了句有点蠢的话:李老师,你怎么确定那个Shape里“operator=”一定存在呢?他不需要定义一下吗?
李老师笑着对我说到:看,这就是没有好好看侯捷老师视频的典型。
四大函数,构造、拷贝、操作符赋值、析构,一旦一个对象被创建这四个函数便被编译器自动生成默认结构了,so……

今天李老师讲的时候,提到了过了很多次解耦思想,一个函数定义的什么功能就只做什么事情,不要直接"left->x = x",搞得你很懂外面那个类是怎么实现的一样,非要去操作底层。很多时候我们在进行团队合作的时候,尤其是大公司,你调用的东西很可能不是你写的,你不知道你用的那个数据何时会发生改动,所以你要保证你的通用性、稳定性。写某个函数的时候,假设其他函数都是正确的,并且你不知道他们的内部实现,你只管调用他们的接口然后完成你当前函数要做的工作。所以说尽量用下面这种写法,否则后患无穷。

this->leftUp = new Point(x, y);
this->leftUp = new Point(*other.leftUp);
Shape::operator = (other);

赋值操作的最后我们仍要谈到喜欢逗你玩的类成员指针变量leftUp。

if(other.leftUp != nullptr)
{
    if(leftUp != nullptr ) {
        *leftUp = *other.leftUp;
    }
    else
    {
        leftUp = new Point(*other.leftUp);
    }
}
else
{
    delete leftUp;
    this->leftUp = nullptr;
}

首先我们要判断other.leftUp是否为空,如果不为空我们就准备进行赋值操作,继续判断当前类成员leftUp是否为空,若为空就new一个直接在堆中初始化,否则直接改变leftUp指向的内容(原来指向的会被析构函数释放掉)。最后,若要赋值的other.leftUp为空,我们就先delete当前类中的leftUp,然后将他指向nullptr。

inline
Rectangle& Rectangle::operator=(const Rectangle& other)
{
    if(this == &other)
    {
        return *this;
    }

    Shape::operator=(other);
    this->width = other.width;
    this->height = other.height;
    
    if(other.leftUp != nullptr)
    {
        if(leftUp != nullptr) {
            *leftUp = *other.leftUp;
        }
        else
        {
            leftUp = new Point(*other.leftUp);
        }
    }
    else
    {
        delete leftUp;
        this->leftUp = nullptr;
    }


    return *this;
}

析构函数

先来点前奏delete p与 delete[] p的区别

下面我们来看程序


inline
Rectangle::~Rectangle()
{
        delete leftUp;
        leftup = nullptr;
    }
}

估计很多人只写了个delete leftUp就完事儿了,以为这样leftUp就被释放指向NULL了,事实真的是这样的?

delete只是对指针的指向空间的释放,并不会改变指针的值,即指针不为NULL,把指针指向的空间释放掉,但是指针的本身内容,即指向空间的地址,是不会改变的;指针为NULL时,没有空间可释放,也就不去释放了,而指针依然有效,指针的内容依然是NULL,在指针的有效域结束时,指针本身所占内存自动被释放。
Is it good practice to NULL a pointer after deleting it?

而有人的喜欢在delete之前判断是否为空,stackoverflow有不少这类问题
Is it safe to delete a NULL pointer?
Is there any reason to check for a NULL pointer before deleting?

csdn也有类似的讨论,看着还挺激烈的:)
我真是孤陋寡闻了,今天才知道NULL指针是可以直接delete的
摘一段给你们看看:
需要判断NULL指针,不是因为要delete,而是因为要访问该指针的内容,确保指针有效。
但是这么做还是没办法处理野指针,因为野指针只有在访问时才能知道是否有问题。所以在delete之后应该立即赋值为NULL。这样既方便以后检查指针是否有效,也可以防止二次delete无效的指针或者栈上的地址,引起的段错误。

总结

  • 测试能反映出很多问题,往往你并不能将你心里所想完美无误的实现出来。
  • 画内存模型图,李老师在以前的先下课提到过,大部分时候你感觉你的程序没问题,编译器也没报错,但是仔细一分析,其实错误百出。有些问题只有到达了一定量级才会被你发现,但是画内存模型分析图可以避免这种尴尬的事情。
  • 眼高手低要不得,看几十遍视频也不一定有亲自实现一遍程序体会的深刻。
  • 其实感觉很有问题没写出来,C++的东西深究起来不得了,很多事情要考虑清楚才能写出完美无误的代码,而这一直是我追求的目标,我先反省下自己。
  • 今天听老师单独给我们分析,谈到一些问题的时候要自己思考没空做笔记,今天写的这些都是记在脑子里的,估计会有些遗漏,还望跟我一起接受指导的同学们批评指出。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容