c++拷贝初始化为什么调用的是普通的构造函数?

看了几个回答,似乎都没有弄清楚拷贝构造和赋值操作符,甚至构造函数,我觉得我又要科普一下我的易记法了。

对于拷贝构造来说,归根结底,落脚点在构造函数上。所以调用拷贝构造的时候,一定是这个对象不存在的时候,如下面这句

那么,a是不存在的,而且是通过其它的bulk_item对象构造出来的,那么则调用的是拷贝构造函数。

那么这里就调用的是赋值操作符,因为a是已经存在的对象了,不需要构造了。

至于下面回答调用构造函数的,就纯属乱说的范围了,虽然在构造函数里面有输出信息,然后打印出来了,但是这个构造函数是bulk_item(10)调用然后打印出来的。

这篇文章主要给大家介绍了关于C++中各种初始化方式的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C++具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。

本文主要给大家介绍了关于C++初始化方式的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

C++小实验测试:下面程序中main函数里a.a和b.b的输出值是多少?

 

答案是a.a是0,b.b是不确定值(不论你是gcc编译器,还是clang编译器,或者是微软的msvc++编译器)。为什么会这样?这是因为C++中的初始化已经开始畸形发展了。

接下来,我要探索一下为什么会这样。在我们知道原因之前,先给出一些初始化的概念:默认初始化,值初始化,零初始化。

T global; //T是我们的自定义类型,首先零初始化,然后默认初始化
 

上面这些不同形式的初始化方式有点复杂,我会对这些C++11的初始化做一下简化:

  • 默认初始化 :如果 T 是一个类,那么调用默认构造函数进行初始化;如果是一个数组,每个元素默认初始化,否则不进行初始化,其值未定义。至于 合成的 默认构造函数初始化数据成员的规则是:1.如果类数据成员存在类内初始值,则用该值初始化相应成员(c++11);2.否则,默认初始化数据成员。
  • 值初始化 :如果 T 是一个类,那么类的对象进行默认初始化( 如果T类型的默认构造函数 不是 用户自定义的,默认初始化之前先进行零初始化 );如果是一个数组,每个元素值初始化,否则进行零初始化。
  • 零初始化 :对于static或者thread_local变量将会在其他类型的初始化之前先初始化。如果T是算数、指针、枚举类型,将会初始化为0;如果是类类型,基类和数据成员会零初始化;如果是数组,数组元素也零初始化。

看一下上面的例子,如果T是int类型,那么global和那些T类型的使用值初始化形式的变量都会初始化为0(因为int是内置类型,不是类类型,也不是数组,将会零初始化,又因为int是算术类型,如果进行零初始化,则初始值为0)而其他的默认初始化都是未定义值。

回到开头的例子,现在我们已经有了搞明白这个例子所必要的基础知识。造成结果不同的根本原因是:foo和bar被它们不同位置的默认构造函数所影响。

foo的构造函数在起初声明时是要求默认合成,而不是我们自定义提供的,因此它属于编译器 合成的 默认构造函数 。而bar的构造函数则不同,它是在定义时被要求合成,因此它属于我们用户 自定义的 默认构造函数 。

前面提到的关于值初始化的规则时,有说明到: 如果T类型的默认构造函数不是用户自定义的,默认初始化之前先进行零初始化 。因为foo的默认构造函数不是我们自定义的,是编译器合成的,所以在对foo类型的对象进行值初始化时,会先进行一次零初始化,然后再调用默认构造函数,这导致a.a的值被初始化为0,而bar的默认构造函数是用户自定义的,所以不会进行零初始化,而是直接调用默认构造函数,从而导致b.b的值是未初始化的,因此每次都是随机值。

这个陷阱迫使我们注意:如果你不想要你的默认构造函数是用户自定义的,那么必须在类的内部声明处使用"=default",而不是在类外部定义处使用。

对于类类型来说,用户提供自定义的默认构造函数有一些额外的“副作用”。比如,对于缺少用户提供的自定义默认构造函数的类,是无法定义该类的const对象的。示例如下:

const exec e;  //错误!缺少用户自定义默认构造函数,不允许定义const类对象

通过开头的例子,我们已经对C++的一些初始化方式有了直观的感受。 C++中的初始化分为6种:零 初始化、 默认初始化、值初始化、直接初始化、拷贝初始化、列表初始化。

零初始化和变量的类型和位置有关系,比如是否static,是否aggregate聚合类型。能进行0初始化的类型的对象的值都是0,比如int为0,double为0.0,指针为nullptr;

现在我们已经了解了几种初始化的规则,下面则是几种初始化方式的使用形式:

1. 默认初始化是定义对象时,没有使用初始化器,也即没有做任何初始化说明时的行为。典型的:

2. 值初始化是定义对象时,要求初始化,但没有给出初始值的行为。典型的:

 

3. 直接初始化和拷贝初始化主要是相对于我们自定义的对象的初始化而言的,对于内置类型,这两者没有区别。对于自定义对象,直接初始化和拷贝初始化区别是直接调用构造函数还是用"="来进行初始化。典型的:

 

对于书本中给出的示例:

 

这里s的初始化书本说是直接初始化,看起来似乎像是拷贝初始化,其实的确是直接初始化,因为直接初始化是用参数来直接匹配某一个构造函数,而拷贝构造函数和其他构造函数形成了重载,以至于刚好调用了拷贝构造函数。

事实上,C++语言标准规定复制初始化应该是先调用对应的构造函数创建一个临时对象,然后拷贝构造函数再将构造的临时对象拷贝给要创建的对象。例如:

*)构造函数会被首先调用,创建一个临时对象,然后拷贝构造函数将这个临时对象复制到a。但是标准还规定,为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,从而忽略调用拷贝构造函数进行优化,这样就完全等价于直接初始化了,当然可以使用-fno-elide-constructors选项来禁用优化。

如果我们将string类型的拷贝构造函数定义为private或者定义为delete,那么就无法通过编译,虽然能够进行优化省略拷贝构造函数的调用,但是拷贝构造函数在语法上还是要能正常访问的,这也是为什么C++ primer第五版第13章拷贝控制13.1.1节末尾442页最后一段话中说:

“即使编译器略过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(例如,不能是priviate的)。

拷贝初始化不仅在使用=定义变量时会发生,在以下几种特殊情况中也会发生:

1.将一个对象作为实参传递给一个非引用的形参;

2.从一个返回类型为非引用的函数返回一个对象;

3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

其实还有一个情况,比如:当以值抛出或捕获一个异常时。

另外还有比较让人迷惑的地方在于vector<string> v2(10),在《C++ Primer 5th》中说这是值初始化的方式,但是仔细看书本,这里的值初始化指的是容器中string元素,也就是说v2本身是直接初始化的,而v2中的10个string元素,由于没有给出初始值,因此标准库对容器中的元素采用了值初始化的方式进行初始化。

只要使用了括号(圆括号或花括号)但没有给出具体初始值,就是值初始化。可以简单理解为括号告诉编译器你希望该对象初始化。

没有使用括号,就是默认初始化。可以简单理解成,你放任不管,允许编译器使用默认行为。通常这是糟糕的行为,除非你真的懂自己在干什么。

4. 列表初始化是C++新标准给出的一种初始化方式,可用于内置类型,也可以用于自定义对象,前者比如数组,后者比如vector。典型的:

 

文章写到这里,读者认真的看到这里,似乎已经懂了C++的各种初始化规则和方式,下面用几个例子来检测一下:

 

试问上面代码中,main程序中的各个输出值是多少?先不忙使用编译器编译程序,根据之前介绍的知识先推断一番:

首先,我们需要明白,对于类来说,构造函数是用来负责类对象的初始化的,一个类对象无论如何一定会被初始化。也就是说,当实例化类对象时,一定会调用构造函数,不论构造函数是否真的初始化了数据成员。故而对于没有定义任何构造函数的自定义类来说,该类的默认构造函数不存在“被需要/不被需要”这回事,它必然会被合成。

  • 对于Init1,由于我们对其没有做任何构造函数的声明和定义,因此会合成默认构造函数。
  • 对于Init2,我们在类内部声明处要求合成默认构造函数,因此也会有合成的默认构造函数。

由于Init1和Init2它们拥有类似的合成默认构造函数,因此它们的ia1.i和ib1.i值相同,应该都是随机值,而ia2.i和ib2.i被要求值初始化,因此它们的值都是0。

  • 对于Init3,我们在类外部定义处要求编译器为我们生成默认构造函数,此默认构造函数为用户自定义的默认构造函数。
  • 对于Init4,我们显式的定义了用户自定义默认构造函数。

由于Init3和Init4它们拥有类似的用户自定义默认构造函数,因此它们的ic1.i和id1.i值相同,应该都是随机值,而ic2.i和id2.i虽然被要求值初始化,但也是随机值。

  • 对于Init5,我们显式的定义了用户自定义默认构造函数,并且使用了构造函数初始化列表来值初始化数据成员。

由于Init5我们为它显式提供了默认构造函数,并且手动的初始化了数据成员,因此它的ie1.i和ie2.i都会被初始化为0。

以上是我们的预测,结果会是这样吗?遗憾的是,结果不一定是这样。是我们哪里出错了?我们并没有错误,上面的程序结果取决于你使用的操作系统、编译器版本(比如gcc-5.0和gcc-7.0)和发行版(比如gcc和clang)。可能有的人能获得和推测完全相同的结果,而有的人不能,比如在经常被批不遵守C++标准的微软VC++编译器(VS 2017,DEBUG模式)下,结果却完全吻合(可能是由于微软开始接纳开源和Linux,逐渐的严格遵守了语言标准),GCC的结果也是完全符合,而广受好评的Clang却部分结果符合。当然,相同的Clang编译器在Mac和Ubuntu下结果甚至都不一致,GCC在某些时候甚至比Clang还人性化的Warning告知使用了未初始化的数据成员。

虽然,上面程序中有一些地方因为操作系统和编译器的原因和我们预期的结果不相同,但也有必然相同的地方,比如最后一个使用了构造函数初始化列表的类的行为就符合预期。还有在合成的默认构造函数之前会先零初始化的地方,必然会初始化为0。

至此,我们已经对C++的初始化方式和规则已经有了一个了然于胸的认识,那就是:由于平台和编译器的差异,以及对语言标准的遵守程度不同,我们决不能依赖于合成的默认构造函数。这也是为什么C++ Primer中多次强调我们不要依赖合成的默认构造函数,也说明了C++ Primer在关于手动分配动态内存那里告诉我们,对于我们自定义的类类型来说,为什么要求值初始化是没有意义的。

C++语言设计的一个基本思想是“自由”,对于某些东西它既给出了具体要求,又留出了发挥空间,而那些未加以明确的地方是属于语言的“灰暗地带”,我们需要小心翼翼的避过。在对象的初始化这里,推荐的做法是将默认构造函数删除,由我们用户自己定义自己的构造函数,并且合理的初始化到每个成员,如果需要保留默认构造函数,一定要对它的行为做到心里有数。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。


1. 比较值传递和引用传递的相同点和不同点。

值传递是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将 实参的值传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱 离关系,此后无论形参发生了怎样的改变,都不会影响到实参。
引用传递将引用作为形参,在执行主调函数中的调用语句时,系统自动用实参来初始化 形参。这样形参就成为实参的一个别名,对形参的任何操作也就直接作用于实参。


2 什么叫内联函数?它有哪些特点?

定义时使用关键字 inline 的函数叫做内联函数;编译器在编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销;
内联函数体内不能有循环语句和 switch 语句;
内联函数的定义必须出现在内联函数第一次被调用之前;对内联函数不能进行异常接口声明;


3 什么叫复制构造函数?复制构造函数何时被调用?

复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的 对象的引用 (const T &)。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
(1) 当用类的一个对象去初始化该类的另一个对象时;
(2) 如果函数的形参是类的对象,调用函数时进行形参和实参结合时;
(3) 如果函数的返回值是类的对象,函数执行完成返回调用者时。


4 复制构造函数与赋值运算符(=)有何不同?

从概念上区分:复制构造函数是构造函数,而赋值操作符属于操作符重载范畴,它通常 是类的成员函数 从原型上来区分:
赋值操作符原型 ClassType& operator=(const ClassType &);返回值为 ClassType 的引用,便于连续赋 值操作 从使用的场合来区分:复制构造函数用于产生对象,它用于以下几个地方:函数参数为 类的值类型时、函数返回值为类类型时以及初始化语句


5 什么叫作用域?有哪几种类型的作用域?

作用域讨论的是标识符的有效范围,作用域是一个标识符在程序正文中有效的区域。 C++的作用域分为:函数原型作用域、函数作用域、文件作用域(全局作用域)、块作用 域(局部作用域)、类作用域和名字空间作用域。


6 什么叫做可见性?可见性的一般规则是什么?

可见性是标识符是否可以引用的问题; 可见性的一般规则是:标识符要声明在前,引用在后,在同一作用域中,不能声明同名 的标识符。对于在不同的作用域声明的标识符,遵循的原则是:若有两个或多个具有包含关 系的作用域,外层声明的标识符如果在内层没有声明同名标识符时仍可见,如果内层声明了 同名标识符则外层标识符不可见。


7 什么叫静态数据成员?他有何特点?

类的静态数据成员是类的数据成员的一种特例,采用 static 关键字来声明。对于类的普
通数据成员,每一个类的对象都拥有一个拷贝,就是说每一个对象的同名数据成员可以分别存储不同的值,这也是保证对象拥有区别于其他对象的特征的需要,但是静态数据成员,每 个类只要一个拷贝,由所有该类的对象共同维护和使用,这个共同维护,使用也就实现了同一类的不同对象之间的数据共享。


8 什么叫静态函数成员?他有何特点?

使用 static 关键字声明的函数成员是静态的,静态函数成员属于整个类,被同一个类的 所有对象共同维护,为这些所有对象共享。
静态成员函数具有以下两个方面的好处:
一是由于静态成员函数只能直接访问同一个类的静态数据成员,可以保证不会对该类的其余数据成员造成负面影响;
二是同一个类只维护一个静态函数成员的拷贝,节约了系统的开销,提高 程序的运行效率。


9 什么叫友元函数?什么叫友元类?

友元函数是使用关键字 friend 关键字声明的函数,它可以访问相应类的保护成员和私 有成员。友元类是使用 friend 关键字声明的类,它的所有成员函数都是相应类的友元函数。
友元不可继承,没有传递性。

10 在函数内部定义的普通局部变量和静态局部变量在功能上有何不同?计算机底层对这两类变量做了怎样的不同处理导致了这种差异?

局部作用域中静态变量的特点是:它并不会随着每次函数调用而产生一个副本,也不会 随着函数的返回而失效,定义时未指定初值的基本类型静态变量,会被以 0 值初始化;局部作用域的全部变量诞生于声明点,结束于声明所在的块执行完毕之时,并且不指定初值,意 味初值不确定。普通局部变量存放于栈区,超出作用域后,变量被撤销,其所占的内存也被 回收;静态局部变量存放于静态数据存储区,全局可见,但是作用域是局部作用域,超出作用域后变量仍然存在。


11 引用和指针有何区别?何时只能使用指针而不能使用引用?

引用是一个别名,不能为 NULL 值,不能被重新分配;
指针是一个存放地址的变量。
当需要对变量重新赋以另外的地址或赋值为 NULL 时只能使用指针。
引用是一个变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间。


12 比较类的 3 种继承方式之间的差别?

公有继承,使得基类 public(公有)和 protected(保护)成员的访问属性在派生类中不变, 而基类 private(私有)成员不可访问。
私有继承,使得基类 public(公有)和 protected(保护)成员都以 private(私有)成员身份出 现在派生类中,而基类 private(私有)成员不可访问。


13 什么叫虚基类?有何作用?

当某类的部分或全部直接基类是从另一个基类派生而来,这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,派生类的对象的这些同名成员在内存中同时拥有多 个拷贝,我们可以使用作用域分辨符来唯一标识并分别访问它们。
我们也可以将直接基类的共同基类设置为虚基类,这时从不同的路径继承过来的该类成员在内存中只拥有一个拷 贝,这样就解决了同名成员的唯一标识问题。
虚基类的声明是在派生类的定义过程,其语法格式为: class 派生类名:virtual 继承方式 基类名 上述语句声明基类为派生类的虚基类,在多继承情况下,虚基类关键字的作用范围和 继承方式关键字相同,只对紧跟其后的基类起作用。
声明了虚基类之后,虚基类的成员在 进一步派生过程中,和派生类一起维护一个内存数据拷贝。 虚基类就是为了解决多继承产生的二义性问题


14 组合与继承有什么共同点和差异?通过组合生成的类与被组合的类之间的逻辑关系是什 么?继承呢?

组合和继承它们都使得已有对象成为新对象的一部分,从而达到代码复用的目的。组合 和继承其实反映了两种不同的对象关系。
组合反映的是“有一个”(has-s)的关系,如果类 B 中存在一个类 A 的内嵌对象,表示的 是每一个 B 类型的对象都“有一个”A 类型的对象,A 类型的对象与 B 类型的对象是部分整体 的关系。
继承反映的是“是一个”(is-a)的关系,在“是一个”关系中,如果类 A 是类 B 的公有基 类,那么这表示每一个 B 类型的对象都“是一个”A 类型的对象,B 类型的对象与 A 类型的对 象是特殊与一般的关系。


15 基类与派生类的对象,指针或引用之间,哪些情况下可以隐含转换,哪些情况下可以显 示转换?在涉及多重继承或虚继承的情况下,在转换时会面临哪些新问题?

派生类指针可以隐含转换为基类指针,而基类指针要想转换为派生类指针,则转换一定 要显示地进行。因为从特殊的指针转换到一般的指针时安全的,因此允许隐含转换;
从一般 的指针转换到特殊的指针是不安全的,因此只能显示地转换。基类对象一般无法被显式转换 为派生类对象。在多重继承情况下,执行基类指针到派生类指针的显式转换时,有时需要将 指针所存储的地址值进行调整后才能得到新指针的值。
但是,如果 A 类型是 B 类型的虚拟 基类,虽然 B 类型的指针可以隐含转换为 A 类型,但是 A 类型指针却无法通过 static_case 隐含转换为 B 类型的指针。


16 什么叫做多态性?在 C++种是如何实现多态的?

**多态是指同样的消息被不同类型的对象接收时导致完全不同的行为,是对类的特定成员 函数的再抽象。**C++支持重载多态,强制多态,包含多态和参数多态。 在基类中声明相应的函数为 virtual 型,然后在派生类中实现该函数,这样就可以通过 基类指针调用派生类对象的函数,实现了运行时动态绑定,即多态的功能。


17 什么叫抽象类?抽象类有何作用?抽象类的派生类是否一定要给出纯虚函数的实现?

带有纯虚函数的类是抽象类。其主要作用是通过它为一个类族建立一个公共的接口,使 他们能够更有效地发挥多态特性。抽象类声明了一个类族派生类的共同接口,而接口的完整 实现,即纯虚函数的函数体,要由派生类自己定义。抽象类派生出新的类之后,如果派生类 给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反 之,如果派生类没有给出全部纯虚函数的实现,此时的派生类仍然是一个抽象类。


18 在 C++中,能否声明虚构造函数?为什么?能否声明虚析构函数?有何用途?

在 C++中,不能声明虚构造函数,多态是不同的对象对同一消息有不同的行为特性,虚 函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的, 因此虚构造函数是没有意义的;
可以声明虚析构函数,析构函数的功能是在该类对象消亡之前进行一些必要的清理工作, 如果一个类的析构函数是虚函数,那么,由它派生而来的所有子类的析构函数也是虚函数。 析构函数设置为虚函数之后,在使用指针引用时可以动态联编,实现运行时的多态,保证使 用基类的指针就能够调用适当的析构函数针对不同的对象进行清理工作。


19 什么叫做流?流的提取和插入是指什么?I/O 流在 C++中起着怎样的作用?

流是一种抽象,它 负责在数据的生产者和数据的消费者之间建立联系,并管理数据的流 动,一般意义下的读操作在流数据抽象中被称为(从流中)提取,写操作被称为(向流中) 插入。
操作系统是将键盘、屏幕、打印机和通信端口作为扩充文件来处理的,I/O 流类就是 用来与这些扩充文件进行交互,实现数据的输入与输出。


20 什么叫做异常?什么叫做异常处理?

当一个函数在执行的过程中出现了一些不平常的情况,或运行结果无法定义的情况,使 得操作不得不被中断时,我们说出现了异常。异常通常是用 throw 关键字产生的一个对象, 用来表明出现了一些意外的情况。我们在设计程序时,就要充分考虑到各种意外情况,并给 与恰当的处理。这就是我们所说的异常处理。


21 C++的异常处理机制有何优点?

C++的异常处理机制使得异常的引发和处理不必在同一函数中,这样底层的函数可以着 重解决具体问题,而不必过多地考虑对异常的处理。上层调用者可以在适当的位置设计对不 同类型异常的处理。


22 比较函数重载和虚函数在概念和使用方式方面有什么区别?

    函数重载可以用于普通函数(非成员的函数)和类的成员函数,而虚函数只能用于 类的成员函数。
    函数重载可以用于构造函数,而虚函数不能用于构造函数。
    如果对成员函数进行重载,则重载的函数与被重载的函数应当都是同一类中的成员 函数,不能分属于两个不同继承层次的类。函数重载是横向的重载。虚函数是对同 一类族中的基类和派生类的同名函数的处理,即允许在派生类中对基类的成员函数 重新定义。虚函数的作用是处理纵向的同名函数。
    重载的函数必须具有相同的函数名,但函数的参数个数和参数类型二者中至少有一 样不同,否则在编译时无法区分它们。而虚函数则要求再同一类族中的所有虚函数 不仅函数名相同,而且要求函数类型、函数的参数个数和参数类型都全部相同,否 则就不是重定义了。也就不是虚函数了。
    函数重载是在程序编译阶段确定操作的对象的,属静态关联。虚函数是在程序运行 阶段确定操作的对象的,属动态关联。

24 浅拷贝和深拷贝的区别?

在某些情况下,类内的成员变量需要动态开辟堆内存,如果实行按位拷贝,就可能会出 现两个对象里面的成员变量指针指向同一块内存区域。当回收其中一个对象时,该对象成员 变量所指向的内存区域也将回收,会导致另一个对象存在野指针的情况。非常危险。 深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程 的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

    定义 const 常量,具有不可变性,保护被修饰的东西。防止意外修改,增强程序的 健壮性。
    便于类型检查,使编译器对处理内容有更多的了解。
    可以避免意义模糊的数字出现,也便于进行参数的修改。同宏定义一样,可以做到 不变则以,一变都变。
    const 定义的常量在程序运行过程中只有一份拷贝,而宏定义却又若干份拷贝。

如果基类要被派生,则析构函数一定要带 virtual。否则,通过基类指针指向派生类对象 后,删除该指针时并不会释放派生类对象的空间。也即,不会调用派生类的析构函数。

27 重写,重载和隐藏的区别?

重载:在一个类中的同名函数,但参数列表不同,函数的返回值相同。
隐藏:派生类的函数屏蔽了与其同名的父类函数
重写(覆盖):父类和子类存在相同的函数,包括返回值,参数列表均相同,父类的该 函数必须含有 virtual 关键字。


28 C++中的四个默认的函数

29 不可以被重载的运算符–五个

类属关系运算符、成员指针运算符“、作用域运算符、 sizeof 运算符和三目 运算符


30 三种字符数组初始化的方法


31 哪些操作符必须重载为成员函数


32 什么是“else 摇摆问题”,举例说明

C++编译器总是把 else 同与它最近的 if 联系起来,因此如果没有正确放置花括号对就 会出现逻辑错误。


33 函数模板和函数重载的区别与联系

函数重载:C++允许使用同一个函数名,不同的参数个数和参数类型来定义多个函数。 重载函数的参数个数,类型,顺序至少有一个不同。返回值可以相同,也可以不同。

函数模板:是对一类同构函数的抽象定义,并不是具体的函数,函数模板的定义被编译 时不会产生任何可执行的代码。 联系: 针对不同类型的数据,但实现相同功能的函数重载可以通过函数模板来替代。


34 怎样区别虚函数和纯虚函数?两者都有什么作用

纯虚函数是在基类中声明的虚函数,在基类中没有定义。在基类中实现纯虚函数的方法 是在函数原型后加“=0。虚函数可以实现也可以不实现。
定义纯虚函数是为了实现一个接口,起到了一个规范的作用。规范继承这个类的程序员 必须实现这个接口。
声明了纯虚函数的类为抽象类,不能实例化对象。 定义虚函数是为了允许用基类的指针来调用子类的这个函数。
抽象类的作用:将有关的操作作为一个接口组织在一个继承层次结构中,由他来为派生 类提供一个公共的根。派生类将具体实现在其基类中作为接口的操作。


35 面向对象程序“接口与实现方法分离”,有什么作用(好处)

接口与实现分离技术可以只把接口暴露给用户,具体的实现细节隐藏起来,当需要改动 代码时,只要在实现部分修改后编译,用户无需再编译自己的项目。


36 列出所有与字符串处理有关的头文件


37 C++中显示类型转换

    reinterpret_cast:解释的意思,主要用于不相关类型之间的转换。目标和原始值之 间至少有相同的位数,我们可以将转换之后的值再转换回去,而不像其它 3 种类型 可能会导致精度丢失
    dynamic_cast:运行时检查该转换是否类型安全,但只有在多态类型时合法,主要 用于类层次间的上行和下行转换,还可以用于类之间的交叉转换。在进行类层次间 的上行转换时和 static_cast 效果一样,进行下行转换时,具有类型检查的功能,比 static_cast 更安全。


39 存储类别说明符可划分为两类。

静态存储类别(static,extern)和自动存储类别(auto, register)。这样的变量在程序执行进入到定义它们的程序块时创建,在程序块激活时存在, 在程序块退出时销毁。


40 什么是运算符重载,它如何增强 C++的扩展性?

运算符重载是对已有的运算符赋予多重含义,使得同一个运算符作用于不同类型的数据 时导致不同的行为。 运算符重载的本质是函数重载,可以改变现有运算符的操作方式,以适用于类类型,为 类的用户提供了一个直接的接口,使得用户程序所用的语言是面向问题的,而不是面向机器的,增强了 C++的扩展性。


41 为什么说”继承是 C++面向对象的主要特征之一”?请简要说明.?

继承是一种联结类的层次模型,层次结构的上层是最具有通用性的,而下层的部分,即 后代具有特殊性。类可以从他的祖先那里继承方法和成员变量,也可以增加新的方法是适用 于特殊的需要。如果没有继承,类就缺失了一块层次结构,代码重用和数据共享就贯彻不到 底,有了继承,就会有抽象编程中的多态问题,只有从机制内部真正解决了多态表现问题, 对象的数据封装,信息隐藏,代码重用等招式才能淋漓尽致的发挥出来。才称得上真正的面 向对象编程。


42 作用域辨析有哪些?

    类作用域: 在类的作用域内,类的成员可以被类的所有成员函数直接访问,在类的作用域之外, public 类成员通过对象的句柄之一而引用。句柄可以是对象名称,对象的引用,对象的指针。 对象,引用或指针指定了客户可访问的接口。
    全局名字空间作用域: 声明与任何函数或者类之外的标识符具有全局名字空间作用域,这种标识符对于从其声 明处开始直到文件结尾处为止出现的所有函数都是已知的,即可访问的。位于函数之外的全局变量。函数定义和函数原型都属于该作用域。
    函数作用域: 标签是唯一具有函数作用域的标识符,标签可以在函数中的任何地方使用,但是不能在 函数体外被引用。
    局部作用域: 在一个语句块中声明的作用域,开始于标识符的声明出,结束于标识符声明处所在语句 块的结束右花括号处。局部变量具有局部作用域,函数形参也是函数的局部作用域。当语句 块是嵌套的,并且外层语句块的一个标识符和内层语句块的一个标识符具有相同的名字时, 外层的标识符处于隐藏状态,直到内存的语句块执行结束为止。
    函数原型作用域: 函数原型形参列表中的标识符,在一个函数原型中,一个特定的标识符只能使用一次。

43.STL 是什么?组成部分和区别。

STL 是 c++提供的标准模板库。
STL 的主要组成部分为:容器、迭代器、算法、函数对象和适配器。其中算法处于核心 地位,迭代器如同算法和容器之间的桥梁,算法通过迭代器从容器中获取元素,然后将获取 的元素传递给特定的函数对象的操作,最后将处理后的结果存储到容器中。


44 如何声明和使用虚函数。说明它在多态性中的作用和意义。

声明:在基类中声明成员函数时在前面加上关键字 virtual 。
使用虚函数:在基类中将成员函数声明为虚函数,这样在派生类中重写该方法后,在使 用基类的指针或引用指向派生类对象时,就可以通过这个基类指针或引用访问到派生类的方法。

C++中的多态可分为四类:重载多态、强制多态、包含多态和参数多态,其中包含多态 是研究类族中定义于不同类中的同名函数的多态行为,主要通过虚函数来实现。多态使得接 口与实现得到分离,要利用统一接口实现运行时多态一般需要动态绑定,而虚函数是动态绑 定的基础,就使得虚函数在多态中很重要。

多态指同样的消息被不同类型的对象接收时导致不同的行为。 虚函数机理:当编译器看到 fn 的虚函数标志时,会记下,等遇到这个虚函数的调用时,将 该捆绑操作滞后到运行中,以实际的对象来捆绑其对应的成员函数操作,编译器在捆绑操作 b.fn()处避开函数调用,只作一个指向实际对象成员函数的间接访问,每个实际的对象都需 额外占有一个指针空间,以指向类中的虚函数表。


44.什么是逻辑错误?什么是语法错误?请举例说明。

语法错误:违背了 C++语言的规定,不能生成可执行文件,主要是语句的结构或拼写中 存在的错误。 例如:for( int i = 0 , i < 10 , i ++) { } //应该用“;


45 编写语句说明枚举类型是如何定义和使用的。

枚举类型的声明形式如下 enum 枚举类型名 { 变量值列表 } ;

46 ADT是什么?简述你对“数据抽象”和“信息隐藏”的认识。

C++内部的数据类型包括基本类型和复合类型(数组、字符串、指针和结构),有表示 范围,只是用可以接受的方式表示实际概念。确定了一个类型就确定了计算机存储给类型所 需要的容量,确定了其表示范围,也确定了对该类型可以进行的操作。

抽象数据类型(Abstract Data Type),简称 ADT,是指一个数学模型以及定义在该模型 上的一组操作。 通常以以下格式定义: ADT 抽象数据类型名{ 数据对象:<数据对象的定义> 数据关系:<数据关系的定义> 基本操作:<基本操作的定义> }

数据抽象:对具体事物描述的一个概括。通过数据抽象可以将数据类型的定义和它的实 现分开,使得只研究和使用它的结构而不用考虑它的实现细节成为可能。C++中的类就是一 种数据抽象,类是具有相同属性和服务的一组对象的集合。

信息隐藏:C++中的封装就是信息隐藏的一种,即尽可能的隐藏对象的内部细节,对外 形成一个边界,只保留有限的对外接口使之与外部反生关系。


47 简述你对“面向对象”和“面向过程”编程思想的理解和认识。

“面向过程”是一种以事件为中心的编程思想,就是分析出解决问题所需要的步骤,然后 用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。过程化编程强调 功能,以过程模块为中心,分层逐步展开设计。通常采用结构化程序设计,基本思路为:自 顶向下、逐步求精。

“面向对象”的编程思想就是把你需要解决的问题中的所有具体的东西,都看成一个个具 有属性和行为的对象,然后把所有具有相同性质的对象抽象成类,那些与问题无关的对象则 忽略。对象化编程强调分离抽象层次,以便让程序员分工,关心不同抽象层次中的细节,而 不用去关心不同抽象层次的联系,数据安全而隐蔽,不同抽象层次的职责分明。


50 写出递归函数定义以及相应的优缺点

递归函数是直接或间接地(通过另一个函数)调用自己。

结构清晰,可读性强,使程序易于理解和调试。

    效率较低。递归是有时间和空间消耗的。另外,递归有可能很多计算都是重复的,从而给性能带来很大的负面影响。因为递归的本质是把一个问题分解成连个或者多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。

    可能导致调用栈溢出。每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致栈溢出。

51 写出包含输入输出的标准库

52 怎样区别虚函数和纯虚函数?两者都有什么作用

在派生子类中对虚函数和纯虚函数的个性化实现,都体现了“多态”特性。但区别是:

    子类如果不提供虚函数的实现,将会自动调用基类的缺省虚函数实现,作为备选方案;
    子类如果不提供纯虚函数的实现,编译将会失败。尽管在基类中可以给出纯虚函数的实现,但无法通过指向子类对象的基类类型指针来调用该纯虚函数,也即不能作为子类相应纯虚函数的备选方案。(纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路。)

虚函数:当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定。所以虚函数的调用时由指针所指向内存块的具体类型决定的。

纯虚函数:纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。

定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

含有纯虚函数的类称之为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

我要回帖

更多关于 c++初始化结构体 的文章

 

随机推荐