53

C++面向对象FQA

 5 years ago
source link: http://whatbeg.com/2019/04/16/cppfqa.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Q. 面向对象的理解?

面向对象是一种程序设计方法。面向对象有三大特性:封装,继承,多态。

1) 封装:

封装可以隐藏实现细节,使得代码模块化;封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

2) 继承:

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程。

通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。在某些

OOP 语言中,一个子类可以继承多个基类。但是一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。

3) 多态:

多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

封装和继承比较简单,多态这块比较繁琐,多啰嗦一些。

说说多态的作用,多态在C++中其实分为静态多态和动态多态,静态多态其实就是重载,也是C语言没有的功能。

1)动态多态:多态的目的是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,只能访问指针类型所在类的对象。

2)静态多态:通过函数重载来实现

a/ 类的构造函数和类名相同,如果没有重载,实例化不同对象非常麻烦

b/ 操作符重载,可以大大丰富操作符的含义,并且为特殊类指定特殊的运算符

c/ 可能有同一个函数需要处理不同的参数类型,重载后无需改变函数名字,也起到统一接口的作用

多态通过虚函数来实现。即父类定义虚函数,子类继承或重写虚函数。

虚函数的内存结构如下:

在一个类中,成员函数有虚函数的话,那么,这个对象的前四个字节是存放一个指向这个虚函数表(简称虚表)的指针。虚表里面放的是虚函数的地址。即使是存在虚基类指针,虚表指针也是在虚基类指针的上方,这是为了保证正确取到虚函数的偏移量。

C++也允许多继承,但多继承会有菱形继承问题,那么对于菱形继承,有多个基类的类对象,则会有多个虚表,每一个基类对应一个虚表,同时,虚表的顺序和继承时的顺序相同。但是菱形继承会造成数据冗余和二义性,虚继承可以解决菱形继承的数据冗余与二义性。

Q. 什么函数不能声明为虚函数?

1) 构造函数:因为先构造父类然后构造子类,所以父类必须有实构造函数

2) 静态成员函数:静态成员属于类,不属于对象,没有多态一说

3) 内联函数:内联函数涉及到展开,在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。

同时,析构函数可以调用虚函数,而构造函数不可以调用虚函数,理由在于,创建类对象时,会先构造父类子对象再构造该类自己,构建父类子对象的时候,如果构造函数包含虚函数,这个虚函数不会解析为子类重写过的函数,而是父类本身的函数。即,父类对象构造期间虚函数绝不会下降到子类层。从而引起非预期的结果。

在析构函数中,不仅可以调用虚函数,而且非常建议(甚至必须)在有虚函数的时候,创造一个虚析构函数。

因为如果对象经由父类指针被delete时,如果析构函数非虚,则可能只会析构父对象部分,子类成分很可能没被销毁!(C++官方解释是,未有定义)如果基类的析构函数是虚函数,则会在运行时多态的影响下调用派生类的析构函数。

所以,只要类有一个虚函数,都应该将析构函数声明为虚函数。

多态是有代价的,C++访问虚函数比访问普通函数慢,原因如下:

1)多了几条汇编指令(运行时得到对应类的函数的地址,取虚表,取虚函数地址,call调用)。

2)影响CPU流水线(这个没有展开,有兴趣的可以自己查一下)

3)编译器不能内联优化(仅在用父类引用或指针调用时,不能内联,是因为在得到子类的引用或者指针之前,根本不知道要调用哪个函数,所以无从内联,但是值得注意的是:对于子类直接调用虚函数,是可以内联优化的。)

再说说,构造,析构,拷贝构造等函数。

C++ 类中默认自带的6个函数

构造函数,析构函数,拷贝构造函数,重载赋值=运算符,取地址运算符,const修饰的取地址运算符

构造函数,拷贝构造函数,析构函数是子类不能继承父类的函数

异常经常会出现,那么在构造函数和析构函数中能否抛出异常呢?

Q. 构造函数是否可以抛出异常?

1、构造函数中抛出异常,对象的析构函数将不会被执行。

2、尽量不要让异常处理离开构造函数,应该再构造函数内部就地处理

3、构造函数抛出异常时,本应该在析构函数中被delete的对象没有被delete,会导致内存泄露。

Q. 析构函数是否可以抛出异常?

1、 不要在析构函数中抛出异常!虽然C++并不禁止析构函数抛出异常,但这样会导致程序过早结束或出现不明确的行为。

2、 如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。

3、 如果析构函数中异常非抛不可,那就用try catch来将异常吞下,但这样方法并不好,我们提倡有错早些报出来。

总结

1、 构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!

2、 不要在析构函数中抛出异常!

构造函数中,成员变量一定要通过初始化列表来初始化的情况:

构造函数中,成员变量一定要通过初始化列表来初始化的有以下几种情况:

1、const常量成员,因为常量只能初始化,不能赋值,所以必须放在初始化列表中;

2、引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表中;

3、没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数;

Q. 临时对象在什么时候会产生?

(1) 用构造函数来做隐式转换函数时,会创建临时对象;

(2) 建立一个没有命名的非堆(non-heap)对象,也就是无名对象时,会产生临时对象;

(3) 函数返回一个对象值时,会产生临时对象,函数中的返回值会以值拷贝的形式拷贝到被调函数栈中的一个临时对象。

Q. 成员函数里memset(this,0,sizeof(*this))会发生什么?

有时候类里面定义了很多int,char,struct等C语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的:

1.类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常

2.类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

拷贝构造函数在哪几种情况下会被调用?

在C++中,下面三种情况会调用拷贝构造函数(有时也称“复制构造函数”):

1) 一个对象作为函数参数,以值传递的方式传入函数体;

2) 一个对象作为函数返回值,以值传递的方式从函数返回;

3) 一个对象用于给另外一个对象进行初始化(常称为复制初始化);

Q. 什么时候必须重写拷贝构造函数?

一个比较简单的原则:如果你需要定义一个非空的析构函数,那么,通常情况下你也需要定义一个拷贝构造函数。

更通常的原则是:

1)对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;

注意,在提供拷贝构造函数的同时,还应该考虑重载”=”赋值操作符号。

Q. 何时编译器会自动生成默认构造函数?

其实默认构造函数也是分为两类的:有用的、无用的。所谓有用的标准也是就默认构造函数会为我们的类做一些初始化操作。那么无用的就不会做任何工作,从而对我们的类也就没有任何意义。所以,我们通常所说的默认构造函数是指有用的默认构造函数,其英文名字叫 nontrivial default constructor。

但是,对于以下四种情况,编译器会自动生成默认构造函数:

1)如果一个类没有任何构造函数,但是含有一个类类型的成员变量,该成员对象有nontrivial default constructor,此时编译器会为该类合成一个默认的构造函数;

2)如果一个类没有任何构造函数,但是该类继承自含有默认构造函数的基类,该基类有nontrivial default constructor,此时编译器会为该类合成一个默认的构造函数;

编译器这样的理由是:因为派生类被合成时需要显式调用基类的默认构造函数。

3)如果一个类没有任何构造函数,但是该类声明或继承了虚函数,含有任何virtual function table(或vtbl)、pointer member(或vptr),此时编译器会为该类合成一个默认的构造函数;

编译器这样做的理由很简单:因为这些vtbl或vptr需要编译器隐式(implicit)的合成出来,那么编译器就把合成动作放到了默认构造函数里面。所以编译器必须自己产生一个默认构造函数来完成这些操作。

4)如果一个类没有任何构造函数,但是该类含有虚基类,此时编译器会为该类合成一个默认的构造函数;

除了以上四种情况,编译器并不会为我们的类产生默认构造函数。

Q. 何时编译器会自动生成拷贝构造函数?

只有在4种情况下编译器才会给我们生成缺省拷贝构造函数:

1)类包含的成员变量是object,并且这个object的类有拷贝构造函数。

2)类继承自一个基类,这个基类有拷贝构造函数。

3)类声明了1个或者多个虚函数。

4)类继承自一个基类,这个基类有1个或者多个虚函数。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK