例 1: C++ 中的空类默认产生哪些类成员函数?
对于一个空类,编译器默认产生 4 个成员函数:默认构造函数、析构函数、拷贝构造函数和赋值函数。
例 2: structure 是否能够领有 constructor / destructor 及成员函数?如果能够,那么 structure 和 class 还有区别么?
区别是 class 中变量默认是 private, struct 中的变量默认是 public。struct 能够有构造函数,析构函数,之间也能够继承,等等。C++ 中的 struct 其实和 class 意义一样,惟一不同的就是 struct 外面默认的访问控制是 public, class 中默认的访问控制是 private。C++ 中存在 struct 关键字的惟一意义就是为了让 C 程序员们有个归属感,是为了让 C ++ 编译器兼容以前用 C 开发的我的项目。
例 3: 现有以下代码,则编译时会产生谬误的是?
struct Test
{Test(int) {}
Test() {}
~Test() {}
void fun() {}
};
int main()
{Test a(1);
a.fun();
Test b();
b.fun();
return 0;
}
Test b()
这个语法等同于申明了一个函数,函数名为 b, 返回值为 Test, 传入参数为空。然而实际上,代码作者是心愿申明一个类型为 Test,变量名为 b 的变量,应该写成Test b;
, 但程序中这个谬误在编译时是检测不进去的。出错的是b.fun()
,它是编译不过来的。
例 4: 上面程序的打印出的后果是什么?
#include <iostream>
using namespace std;
class Base
{
public:
Base(int i) : m_j(i), m_i(m_j) {}
Base() : m_j(0), m_i(m_j) {}
~Base() {}
int get_i() const { return m_i;}
int get_j() const { return m_j;}
private:
int m_i;
int m_j;
};
int main()
{Base obj(98);
cout << obj.get_i() << endl;
cout << obj.get_j() << endl;
return 0;
}
本题想要失去的后果是 ”98,98″。然而成员变量的申明是先 m_i, 而后是 m_j;初始化列表的初始化变量程序是依据成员变量的申明程序来执行的,因而 m_i 会被赋予一个随机值。更改一下成员变量的申明程序能够失去料想的后果。如果要失去 “98,98” 的输入后果,程序须要批改如下
int m_j;
int m_i;
补充变量未初始化的值
- 未初始化的全局数组为 0;
- 未初始化的部分数组为随机值;
- 初始化局部的全局数组与部分数组,初始化局部为初始化值,未初始化局部都为 0;(不论全局还是部分)
- 全局变量未初始化:
#include <iostream>
using namespace std;
int i;
char c;
float f;
double d;
int* p;
int main()
{
cout << i << endl; // 0
cout << c << endl; // '\0'
cout << f << endl; // 0
cout << d << endl; // 0
cout << p << endl; // 0
return 0;
}
- 局部变量未初始化为随机数。
#include <iostream>
using namespace std;
int main()
{
int i;
char c;
float f;
double d;
int* p;
cout << i << endl;
cout << c << endl;
cout << f << endl;
cout << d << endl;
cout << p << endl;
return 0;
}
-
类的成员变量未初始化的值
- 对象在全局作用域或为动态部分对象时,类的内置成员变量被初始化为 0.
- 对象在部分作用域定义时,类的内置成员变量不被初始化为 0。
-
对于类类型成员依照其本身的默认构造函数进行初始化。
<span style=”color:red;”> 如果类显式提供了带参数的构造函数,则编译器不会再为其生成空参数的构造函数。这时候就不能用空参数来定义类类型变量 </span>
class A{ public: int value; A (int i):value(i){}}; int main(){ A a; // error return 0; }
参考代码:
#include <iostream>
using namespace std;
class A
{
public:
int i;
float f;
double d;
char c;
int* p;
void print() {
cout << i << endl;
cout << c << endl;
cout << f << endl;
cout << d << endl;
cout << p << endl;
cout << "-------------------" << endl;
}
};
A a0;
int main()
{a0.print();
A a;
a.print();
return 0;
}
例 5: MFC 类库中,CObject 类的重要性不言自明。在 CObject 的定义中,咱们看到一个乏味的景象,即 CObject 的析构函数是虚构的。为什么 MFC 的编写者认为虚构的析构函数是必要的?
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base" << endl;}
~Base() { cout << "~Base" << endl;}
};
class Derived : public Base
{
public:
Derived() { cout << "Derived" << endl;}
~Derived() { cout << "~Derived" << endl;}
};
int main()
{Base *b = new Derived();
delete b;
return 0;
}
咱们将一个父类的指针指向子类的对象,当咱们应用delete b
去开释 b 指向的内存,会调用父类的析构函数(动态联编),然而却不会调用子类的析构函数。
如果子类对象在构造函数内申请了一块堆内存,最初根据上述状况则会造成内存透露,咱们在 ~Base() { cout << "~Base" << endl;}
前加上 virtual
, delete b
会先调用子类析构,再调用父类析构,天然就把子类申请的内存开释掉啦!
补充常识: C++ 动态联编和动静联编
依据: https://blog.csdn.net/erlian1… 所批改
联编是指一个程序本身彼此关联的一个过程。依照联编所进行的阶段不同,可分为动态联编和动静联编两种。
动态联编: 动态联编是指在程序编译链接阶段进行联编。这种联编又称为晚期联编,这是因为这种联编工作是在程序运行之前实现的。
编译时所进行的联编又称为动态束定。束定是指确定所调用的函数与执行该函数代码之间的关系。其长处是效率高,但灵活性差。
上面来看一个动态联编的程序例题:
#include <iostream>
using namespace std;
class Point
{
public:
Point(double i,double j)// 基类的构造函数
{x=i;y=j;}
double Area() const// 定义的常成员函数
{return 0.0;}
private:
double x,y;
};
class Rectangle:public Point// 私有继承的派生类
{
public:
Rectangle(double i,double j,double k,double l);// 申明派生类的构造函数
double Area() const
{return w*h;}
private:
double w,h;
};
Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)// 派生类构造函数的函数体
{
w=k;
h=l;
}
void fun(Point &s)// 定义的类外函数
{cout<<s.Area()<<endl;
}
int main()
{Rectangle rec(3.5,15.2,5.0,28.0);// 定义的派生类的带参数的对象
fun(rec);// 调用函数 fun()}
输入的后果为:0
程序剖析:从输入的后果来看,该程序执行了 Point::Area()的后果。派生类 Rectangle 的对象 rec 作为函数 fun()的实参,而该函数的形参是类 Point()的对象的援用 s。在程序编译阶段,对象援用 s 所执行的 Area()操作被联编到 Point 类的函数上。因而在执行函数 fun()中的 s.Area()操作时,返回值为 0。
通过对象指针进行的一般成员函数的调用,仅仅与指针的类型无关,而与此刻指针正指向什么对象无关。要想实现当指针指向不同对象时执行不同的操作,就必须将基类中相应的成员函数定义为虚函数,进行动静联编。
动静联编
动静联编是指在程序运行时进行的联编,这种联编又称为早期联编。动静联编的长处是灵活性强,但效率低。
动静联编要求在运行时解决程序中的函数调用与执行该函数代码间的关系,又称为动静束定。在上述的例题中,因为是动态联编,函数 fun()中的参数 s 所援用的对象被联编到类 Point 上。那么履行动静联编,则 s 所援用的对象将被联编到类 Rectangle 上。由此可见,对于同一个对象的援用,采纳不同的联编形式将会被联编到不同类的对象上,即不同联编能够抉择不同的实现,这便是多态性。实际上是对于函数 fun()的参数的多态性抉择。联编是一种重要的多态性抉择。
实现动静联编须要同时满足以下三个条件:
- 必须把动静联编的行为定义为类的虚函数。
- 类之间应满足子类型关系,通常体现为一个类从另一个类私有派生而来。
- 必须先应用基类指针指向子类型的对象,而后间接或者间接应用基类指针调用虚函数。
上述的例题能够改为:
#include <iostream>
using namespace std;
class Point
{
public:
Point(double i,double j)// 基类的构造函数
{x=i;y=j;}
virtual double Area() const// 定义的虚函数
{return 0.0;}
private:
double x,y;
};
class Rectangle:public Point// 私有继承的派生类
{
public:
Rectangle(double i,double j,double k,double l);// 申明派生类的构造函数
virtual double Area() const// 派生类的虚函数
{return w*h;}
private:
double w,h;
};
Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)// 派生类构造函数的函数体
{
w=k;
h=l;
}
void fun(Point &s)// 定义的类外函数
{cout<<s.Area()<<endl;
}
int main()
{Rectangle rec(3.5,15.2,5.0,28.0);// 定义的派生类的带参数的对象
fun(rec);// 调用函数 fun()}
输入的后果为:140.
程序剖析:该程序中阐明了虚函数,fun()函数的对象援用参数 s 被动静联编,该函数体内调用的 Area()函数是在运行中确定的,而不是在编译时确定的,因而在运行时,实参 rec 为类 rectangle 的对象,于是 Area()函数被确定为类 Rectangle 的 Area()函数。
例 6: 析构函数能够为 virtual 型,构造函数则不能。那么为什么构造函数不能为虚呢?
虚函数采纳一种虚调用的方法。虚调用是一种能够在只有局部信息的状况下工作的机制,特地容许咱们调用一个只晓得接口而不晓得其精确对象类型的函数。然而如果要创立一个对象,你势必要晓得对象的精确类型,因而构造函数不能为虚。
例 7: 如果虚函数是十分无效的,咱们是否能够把每个函数都申明为虚函数?
不行,这是因为虚函数是有代价的:因为每个虚函数的对象都必须保护一个虚表,因而在应用虚函数的时候都会产生个零碎开销。如果仅是个很小的类,且不想派生其余
类,那么基本没必要应用虚函数。
例 8: 析构函数能够是内联函数吗?
析构函数能够是内联函数。
例 9: 重载和笼罩有什么不同?
override 是指派生类重写基类的虚函数, overload 约定成俗地被翻译为“重载",是指编写一个与已有函数同名,然而参数表不同的函数。例如一个函数既能够接管整型数作为参数,也能够接管浮点数作为参数。重载不是一 种面向对象的编程,而只是一种语法规定,重载与多态没有什么间接关系。
例 10: 在 C++中,你不应该从以下哪个抛出异样?
A. Constructor(构造函数)B. Destructor(析构函数)C. Virtual function(虚办法)D. None of the above(以上答案都不对)
构造函数中抛出异样 是有肯定必要的,试想如下状况:
构造函数中 有两次 new 操作,第一次胜利了,返回了无效的内存,而第二次失败,此时因为对象结构尚未实现,析构函数是不会调用的,也就是 delete 语句没有被执行,第一次 new 出的内存就悬在那儿了(产生内存泄露),所以异样处理程序能够将其裸露进去。
构造函数中遇到异样是不会调用析构函数的,一个对象的父对象的构造函数执行结束,不能称之为结构实现,对象结构是不可分割的,要么齐全胜利,要么齐全失败,C++ 保障这一点。对于成员变量,C++ 遵循这样的规定,即会从异样的产生点依照成员变量的初始化的逆序开释成员。举例来说,有如下初始化列表:
A:: A() : ml(), m2(), m3(), m4(), m5() () {};
假设 m3 的初始化过程中抛出异样,则会依照 m2,ml 的顺序调用这两个成员的析构函数。在{}之间产生的未捕获异样,最终会导致在栈的开解时析构所有的数据成员。
解决这样的问题,应用智能指针是最好的,这是因为 auto_ptr
成员是一个对象而不是指针。换句话说,只有不应用原始的指针,那么就不用放心构造函数抛出异样而导致资源泄露。
所以在 C ++ 中,资源泄露的问题个别都用 RAII(资源获取即初始化)的方法:把须要关上/敞开的资源用简略的对象封装起来(这种封装能够同时有多种用途,比方暗藏底层 API 细节,以利于移植)。这能够省去很多的麻烦。
如果不必 RAII, 即便以后构造函数里获取的货色在析构函数里都开释了,如果某天对类有改变,要新减少一种资源,构造函数里个别能适当地获取,但记不记得要在析构函数里相应地开释呢?失误的比例很大。如果思考到构造函数里抛出异样,就更简单了。随着我的项目的不断扩大和工夫的推移,这些细节不可能都记得住,而且,有可能会由他人来施行这样的改变。
- C++中告诉对象结构失败的惟一办法,就是在构造函数抛出异样
- 当对象产生局部结构异样时,曾经结构结束的子对象将会逆序地被析构(即异样产生点后面的对象; 而还没有开始构建的子对象将不会被结构了(即异样产生点前面的对象, 当然它也就没有析构过程了; 还有正在构建的子对象和对象本人自身将进行持续构建(即出现异常的对象),并且它的析构是不会被执行的。
析构函数抛异样
Effective C++ 倡议,析构函数尽可能地不要抛出异样。构想如果对象出了异样, 当初异样解决模块为了保护零碎对象数据的一致性,防止资源透露,有责任开释这个对象的资源,调用对象的析构函数,可当初如果析构过程又再出现异常,那么请问由谁来保障这个对象的资源开释呢?而且这新呈现的异样又由谁来解决呢?不要遗记后面的一个异样目前都还没有解决完结,因而陷入了一个矛盾之中,或者说处于有限的递归嵌套之中。所以 C ++ 规范就做出了这种假如。