C++基础概念(下)

简介: C++基础概念(下)

44.拷贝构造函数和赋值运算符的区别

拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。

operator=();是把一个对象(该对象已经存在)赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。

533e85c1c9984aaab2792131a2092479.png

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

一个对象以值传递的方式传入函数体

一个对象以值传递的方式从函数返回

一个对象需要通过另外一个对象进行初始化。


用一个已存在的对象去构造一个不存在的对象(构造之前不存在),就是拷贝构造。用一个已存在的对象去覆盖另一个已存在的对象,就是赋值运算。


需要深拷贝的时候,两个函数需要同时由程序员显示定义。


45.系统调用里有sleep(),usleep()函数,usleep()函数号称自己是微秒级,你相信它真的能达到这么快吗?或者说系统调用可以达到微秒速度吗?

5f333ed6ba92458789bd654bef5ff845.png

a168a35ced1344899ce9edcb9ef0a5df.png


46.重载、覆盖、隐藏

重载:


覆盖(用于虚函数多态),是指派生类函数覆盖基类函数,只作用于派生类函数,其特性为:


隐藏,是指派生累函数将基类函数给藏起来了,当然只作用于派生类函数,其特性与覆盖不同。

不同类中满足重载条件即覆盖,满足虚函数覆盖条件但无virtual也隐藏。

同名参数相同又有virtual那就是覆盖了。


47.this, const成员函数、mutable类型

1.对象调用函数时,会将对象的指针传递初始化this,传递给函数,从而区分里面调用的类的成员到底是哪个对象的。形参this是指向类对象的指针。因为static成员函数是类的组成部分,但不属于任何一个类对象,所以static成员函数没有this指针。虚函数也是通过this调用的,所以static成员函数不能为虚函数。

2.默认的成员函数的this的类型是一个指向类类型的const指针,指针本身不能改变,但指向的内容可以改变,即不能改变对象的地址,能改变对象里的成员。在函数后面加const成为const成员函数,this的类型是一个指向const类类型对象的const指针,对象里的内容也不能改变。(不能调用非const成员函数,因为可能间接改变对象。)

3.若想在const成员函数里改变对象的成员,那么需要在该成员前加mutable关键字,这样即使是在const成员函数中该成员也可以被改变。


举例:

举例请见http://www.time-track.cn/const-this-pointer-mutable-keyword-in-cpp.html


48.STL内存管理

alloc空间分配的策略

考虑到小型区块可能造成的内存的碎片的问题,SGI设计了双层的配置器。

当配置区块超过128字节(Bytes)的时候,视为足够大,调用第一级的配置器;

当相应的配置区块小于128bytes时视为过小,调用第二级配置器。

第一层的配置器直接使用的是malloc()和free(),为了减少内存碎片第二层配置器为,采用相应的Memory pool的方式。


第二级配置器memory pool的管理方式:

(1)free-list:一个类似单链表结构的数据结构,链表中的每一个小的区块都是没有正在被用户使用的(正在使用的将会断开和free-list的连接), 为了管理上的方便,SGI第二层配置器会主动的将任何的小额的区块的内存需求上调为8的倍数(例如需要7byte也给你8byte,有1byte内部碎片)并维护16个free-lists,各自管理大小分别8,16,24…128bytes的小额区块。

db136e97b64c431c8c61ee4415ffbbf5.png

(A)当用户向系统提出使用的需求(nbyte内存)的时候,系统从free-list的头数组中找到对应链表的头指针,若里面没有空闲位置(头指针为空),现在内存池没有加到free-lis的剩余内存中找,如果也没有多余位置再在堆上申请。具体如下:

f8ffa4e70c5247b5930fed64779a98ad.png

内存释放过程:

内存的释放过程比较简单,它接受两个参数,一个是指向要释放的内存块的指针p,另外一个表示要释放的内存块的大小n。分配器首先判断n,如果n>128bytes,则交由第一个分配器去处理;否则将该内存块加到相应的空闲链表中。(空闲链表会越来越大)


49.static 全局变量 和 非static全局变量的区别

1、static 全局变量称:静态外部变量或称静态全局变量。

2、非static全局变量称:外部变量或称全局变量。

3、区别是:用static声明的外部变量,只能供本文件内的函数调用,不能被其它文件的函数所调用。


静态全局变量有以下特点:

• 该变量在全局数据区分配内存;

• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);

• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;


50.编写makefile文件

51.coredump产生的几种可能情况

造成程序coredump的原因有很多,这里总结一些比较常用的经验吧:

1,内存访问越界

a) 由于使用错误的下标,导致数组访问越界。

b) 搜索字符串时,依靠字符串结束符来判断字符串是否结束,但是字符串没有正常的使用结束符。

c) 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操作函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。


2,多线程程序使用了线程不安全的函数。

应该使用下面这些可重入的函数,它们很容易被用错:

asctime_r(3c) gethostbyname_r(3n) getservbyname_r(3n)ctermid_r(3s) gethostent_r(3n) getservbyport_r(3n) ctime_r(3c) getlogin_r(3c)getservent_r(3n) fgetgrent_r(3c) getnetbyaddr_r(3n) getspent_r(3c)fgetpwent_r(3c) getnetbyname_r(3n) getspnam_r(3c) fgetspent_r(3c)getnetent_r(3n) gmtime_r(3c) gamma_r(3m) getnetgrent_r(3n) lgamma_r(3m) getauclassent_r(3)getprotobyname_r(3n) localtime_r(3c) getauclassnam_r(3) etprotobynumber_r(3n)nis_sperror_r(3n) getauevent_r(3) getprotoent_r(3n) rand_r(3c) getauevnam_r(3)getpwent_r(3c) readdir_r(3c) getauevnum_r(3) getpwnam_r(3c) strtok_r(3c) getgrent_r(3c)getpwuid_r(3c) tmpnam_r(3s) getgrgid_r(3c) getrpcbyname_r(3n) ttyname_r(3c)getgrnam_r(3c) getrpcbynumber_r(3n) gethostbyaddr_r(3n) getrpcent_r(3n)


3,多线程读写的数据未加锁保护。

对于会被多个线程同时访问的全局数据,应该注意加锁保护,否则很容易造成coredump


4,非法指针

a) 使用空指针

b) 随意使用指针转换。一个指向一段内存的指针,除非确定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,否则不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易因为bus error而core dump。


5,堆栈溢出(反复递归调用函数而无终止条件)

不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。


52.网络连接中的长链接和短链接。

HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。


53.同步IO和异步IO,阻塞和非阻塞

在处理 IO 的时候,阻塞和非阻塞都是同步 IO。

只有使用了特殊的API才是异步IO。

b8c42c88d8d24a19b9ea3a3174441bc7.png

“阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。

1.同步与异步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。

换句话说,就是由调用者主动等待这个调用的结果。


而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

a. 如果执行部件用状态来通知,

那么调用者就需要每隔一定时间检查一次,效率就很低

有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误。

b. 如果是使用通知的方式,

效率则很高,因为执行部件几乎不需要做额外的操作。

c. 至于回调函数,

和通知没太多区别。


典型的异步编程模型比如Node.js


举个通俗的例子:

你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。

而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。


阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。


还是上面的例子,

你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。

在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。


54.实现栈和队列

栈,用数组实现即可。

队列,用循环数组(满了就重新分配个大的)或者链表实现(永不溢出)


55.大端小端

大端高尾端

小端小尾端

如果把一个数看成一个字符串,比如11223344看成"11223344",末尾是个’\0’,'11’到’44’个占用一个存储单元,那么它的尾端很显然是44,前面的高还是低就表示尾端放在高地址还是低地址,它在内存中的放法非常直观,如下图:

4449fbcb4c6b44ac810dc7093bb6bb15.png

728f86aecba04e5a9369d356a2ba7afb.png

这两种字节序没有标准可循,都有系统在使用。把某个给定系统所用的字节序称为主机字节序,可以用以下程序输出主机字节序。方法是在一个短整数变量中存放2字节的值0x0102,然后查看它的连续字节c[0](对应上图地址A)和c1,以此确定字节序。

aef838a6b05b49e88b63d4eadedd8759.png


56.编译器都会做哪些优化

详细见此

http://www.zhihu.com/question/31941203

57.回调一个对象的成员函数,但是这个对象可能已经不存在了,怎么办?

A:用智能指针吧。

M:什么智能指针?

A:shared_ptr。

M:那如何知道对象不存在了呢?

A:不太清楚。

M:这个就是刚才你不清楚的weak_ptr(expired()是否为false,或者use_count()是否>0)的用处了,可以判断对象是否还存在。


函数非f1调用f2时,还给f2传递了个函数f3(回调函数),f2可以在适当的条件下调用f3返回结果之类的。


你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。


58.介绍一下C++11的特性

1.Auto

2.=default =delete

=default生成默认构造函数,=delete虽然声明了该函数,但不能以任何方式调用它,一般用于拷贝构造函数和=操作符,也可用于除析构函数的其他函数。

3.nullptr

4.for(auto n:v)

5.智能指针:

unique_ptr, shared_ptr, weak_ptr

6.Lambda表达式:

表示一个可调用的代码单元,可以理解为一个未命名的内联函数。

可以出现在一个函数中,通过将局部变量包含在其捕获列表中[]来指出将会使用这些变量。

与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。

1.[var]表示值传递方式捕捉变量var;

2.[=]表示值传递方式捕捉所有父作用域的变量(包括this);

3.[&var]表示引用传递捕捉变量var;

4.[&]表示引用传递方式捕捉所有父作用域的变量(包括this);

5.[this]表示值传递方式捕捉当前的this指针。


1.[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;

2.[&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

3.[=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;

4.[&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。


7.列表初始化

8.正则表达式

9.静态断言:

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。

d62ae355b0424b3fb285406360455a25.png


59.硬盘拷贝速度,内存拷贝速度

硬盘70-80MB/s

内存1GB/s

内存的访问速度比硬盘快,这是毋庸置疑的,但是到底快多少?通常的说法是:内存访问速度是纳秒级(10的-9次方),硬盘的访问速度是微秒级(10的-3次方)。找到一个稍微科学点的测试数据,如下图

30d435fd59de432db9813d0fbeea69a5.png

要对比内存和硬盘的速度,要分为两种请款对比:

1.顺序访问:这种情况下,内存访问速度仅仅是硬盘访问速度的6~7倍(358.2M / 53.2M = 6.7)

2.随机访问:这种情况下,内存访问速度就要比硬盘访问速度快上10万倍以上 (36.7M / 316 = 113,924)


60.Gdb调试,调试core dump文件

GDB 常用操作

上边的程序比较简单,不需要另外的操作就能直接找到问题所在。现实却不是这样的,常常需要进行单步跟踪,设置断点之类的操作才能顺利定位问题。下边列出了GDB一些常用的操作。

启动程序:run

设置断点:b 行号|函数名

删除断点:delete 断点编号

禁用断点:disable 断点编号

启用断点:enable 断点编号

单步跟踪:next 也可以简写 n

单步跟踪:step 也可以简写 s

打印变量:print 变量名字

设置变量:set var=value

查看变量类型:ptype var

顺序执行到结束:cont

顺序执行到某一行: util lineno

打印堆栈信息:bt

最开始gdb+文件名,即可进入调试。

bt这个命令重点推荐,尤其是当函数嵌套很深,调用关系复杂的时候,他能够显示出整个函数的调用堆栈,调用关系一目了然。另外上边有两个单步执行的命令,一个是n,一个是s。主要区别是n会将函数调用当成一步执行,而s会跟进调用函数内部。

bt看到的堆栈信息和栈结构一样,下面的调用上面:


什么是Core Dump?

Core的意思是内存, Dump的意思是扔出来, 堆出来.开发和使用Unix程序时, 有时程序莫名其妙的down了, 却没有任何的提示(有时候会提示core dumped). 这时候可以查看一下有没有形如core.进程号的文件生成, 这个文件便是操作系统把程序down掉时的内存内容扔出来生成的, 它可以做为调试程序的参考.

core dump又叫核心转储, 当程序运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中, 叫core dump.


为什么没有core文件生成呢?

有时候程序down了, 但是core文件却没有生成. core文件的生成跟你当前系统的环境设置有关系, 可以用下面的语句设置一下, 然后再运行程序便成生成core文件.

ulimit -c unlimited

core文件生成的位置一般于运行程序的路径相同, 文件名一般为core.进程号


当获得了core文件以后,就可以利用命令gdb进行查找,参数一是应用程序的名称,参数二是core文件

如: gdb […]xmsd […]/xmsd_PID1065_SIG11.core


然后输入bt或者where找到错误发生的位置和相应的堆栈信息。就可知道发生错误时的函数调用关系,然后可以使用up或者down查看上一条和下一条具体详细信息。这样便能对问题进行大概定位,然后看源代码,进行分析。


构造函数可以调用虚函数吗

da1f383310af466f87577ed2388ae65f.png

语法上可以通过,但是并不会得到你想要的输出:

调用积累的构造函数时输出的并不是覆盖后的function函数。

实际输出:

9fa82ef685aa43b08cf1927d3f9327d0.png

我们知道,在构造一个对象时,过程是这样的:

1) 首先会按对象的大小得到一块内存(在heap上或在stack上),

2) 把指向这块内存的指针做为this指针来调用类的构造函数,对这块内存进行初始化。

3) 如果对象有父类就会先调用父类的构造函数(并依次递归),如果有多个父类(多重继承)会依次对父类的构造函数进行调用,并会适当的调整this指针的位置。在调用完所有的父类的构造函数后,再执行自己的代码。

C++标准规范。在12.7.3条中有明确的规定。这是一种特例,在这种情况下,即在构造子类时调用父类的构造函数,而父类的构造函数中又调用了虚成员函数,这个虚成员函数即使被子类重写,也不允许发生多态的行为。即,这时必须要调用父类的虚函数,而不子类重写后的虚函数。


我想这样做的原因是因为在调用父类的构造函数时,对象中属于子类部分的成员变量是肯定还没有初始化的,因为子类构造函数中的代码还没有被执行。如果这时允许多态的行为,即通过父类的构造函数调用到了子类的虚函数,而这个虚函数要访问属于子类的数据成员时就有可能出错。

相同道理也适用于析构函数,一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定以值,所以c++视他们仿佛不再存在。进入base class析构函数后对象就成为一个base class对象,而c++得任何部分包括virtual函数,dynamic_cast等等也就那么看待它。

在base class构造和析构函数调用期间调用的virtual函数不会下降至derived class。详见effective c++ 第九条。

62.dynamic_cast转换失败时,会出现什么情况?

一个基类对象指针(或引用)cast到继承类指针

对指针,返回NULL。

对引用,抛出bad_cast异常。


63.为什么析构函数不能够抛出异常

more effective c++提出两点理由(析构函数不能抛出异常的理由):

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。


C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。

上面的论述C++异常处理模型它其实是有一个前提假设——析构函数中是不应该再有异常抛出的。试想,如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。


3.2 那么当无法保证在析构函数中不发生异常时, 该怎么办?

其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外(或者使用智能指针将使用到的资源当初对象来看待)。这是一种非常简单,也非常有效的方法。

~ClassName()

{

try{

do_something();

}

catch(){ //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。

}

}


相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
1月前
|
存储 算法 NoSQL
【C/C++ 数据结构 概念】计算机数据结构基础:探索核心概念与术语
【C/C++ 数据结构 概念】计算机数据结构基础:探索核心概念与术语
42 0
|
1月前
|
设计模式 存储 缓存
【C++ 基本概念】深入探索C++ RTTI 特性
【C++ 基本概念】深入探索C++ RTTI 特性
74 0
|
1月前
|
安全 编译器 C++
【C++20概念】编译时多态性的力量
【C++20概念】编译时多态性的力量
54 0
|
1月前
|
算法 安全 编译器
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
63 0
|
15天前
|
设计模式 开发框架 算法
C++中的设计模式:基本概念与应用
C++中的设计模式:基本概念与应用
23 2
|
17天前
|
算法 Java Linux
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现(下)
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现
17 0
|
17天前
|
Java C语言 C++
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现(上)
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现
24 4
|
17天前
|
C语言 C++
从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)(下)
从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)
22 2
|
17天前
|
C语言 容器
从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)(上)
从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)
24 4
|
18天前
|
存储 C语言 Python
从C语言到C++_24(二叉搜索树)概念+完整代码实现+笔试题(下)
从C语言到C++_24(二叉搜索树)概念+完整代码实现+笔试题
36 3