C++中“类”相关知识点汇总

时间:2021-03-14 00:18:33

一:类中默认的成员函数

一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,如果你写

class Empty{};

就相当于:

class Empty
{
public:
Empty(); //缺省构造函数
Empty(const Empty&); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty& rhs);//赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const; //取值运算符const
};

需要注意的是,只有当你需要用到这些函数的时候,编译器才会去定义它们。

在VC编译器中,class A{};单独声明的一个空类A来说,编译器编译过程中,并没有发现创建A实例。所以对于K空类A来说,

编译器是不会给类A生成任何函数的。如果我们在代码中需要生成一个A的实例比如

1) A  a; 编译器就会根据上面的实例,给类A生成构造函数和析构函数。

2) A b(a) ;编译器 就会生成类A的拷贝构造函数。

3) A c; c=a;编译器会生成赋值运算符函数

4) A &d=a;编译器生成取地址运算符函数

经过分析,可以这样理解,对于一个没有实例化的空类,编译器是不会给它生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数。

c++中如何阻止一个类被实例化?

答:使用抽象类或者构造函数声明为private

一般什么时候构造函数被声明成private?

答:比如要阻止编译器生成默认拷贝构造函数的时候

什么时候编译器会生成默认的拷贝构造函数?

答:只要自己没写,而程序中需要,都会生成。

(在C#中,有一个专门的Seal class封闭类来防止继承)

二:类中拷贝构造函数和赋值运算符函数的区别

写法一:T c=a+b或T c(a+b)调用了copy constructor

写法二:T c; c = a+b;调用的是assignment operator

可以先这样通俗地理解:如果一个新的对象被定义(就象上面那行代码中的 c),一个构造函数必须被调用;它不可能是一个赋值。如果没有新的对象被定义(就象上面那行 "c= a+b" 代码中),没有构造函数能被调用,所以它就是一个赋值。
   在inside the c++ object model  6.3文中对他们的解释是:

对于写法1,编译器会产生一个临时对象,放置a+b,然后调用c的copy constructor把临时对象当做c的初始值:

/编译器附加代码:

T _temp;  //产生临时对象

_temp = a+b;  //放置a+b的值

T c(_temp);  //调用c的copy constructor

还有一种可能是直接以拷贝构造的方式,将a+b的值放到c中。

T c(a+b);

或者实行NRV优化:【优化这里不懂】

_result.T::T();

//直接计算_result

C++standard允许编译器厂商有完全的*度,但是由于市场的竞争,几乎保证任何表达式如果有这种形式:
T c = a+b;

那实现时根本不需要产生一个临时对象。

但是对于写法2,不能忽略临时对象。它会导致下面结果:

T temp;

temp.operator+(a,b);

c.operator=(temp);

temp.T::~T();

注意,此时在写法1中的"直接以拷贝构造方式来去除临时对象"或"以NRV优化来消除临时对象"的方法被作用于临时对象temp上。导致只能消除T temp = a+b此表达式的临时对象,而真正的临时对象temp无法被消除。

不管哪一种情况,直接传递c到运算符函数是有问题的。由于运算符函数并不为外加参数调用一个destructor(它期望一块新鲜的内存),所以必须在此调用前先调用destructor。如果将c直接传递到运算符函数中,必须为c调用destructor:【不是很懂】

//C++伪码

c.T::~T();

c.T::T(a+b);  //以destructor和copy
constructor来取代copy
assignment operator

因为copy
constructor、destructor以及copy assignment
operator都可以由使用者供应,因此不能保证上述两个操作符合编译器所需要的语意,因此以一连串的destructor和copy
constructor来取代assignment一般而言是不安全的,而且会产生临时对象。

因此T
c = a+b;

总是比下面的操作更有效率地被编译器转换:

T c;

c= a+b;

注:何为新鲜的内存?意指只分配好的内存,尚未赋值。例如:
char *p=new char[size_t];就是一块新鲜的内存,因为它只执行了下面一步:

char *p = _new (size_t);  //调用operator new 来分配内存

而对于下面这一步:

*p = char::constructor()  //不会进行,因为char无default
构造函数,或者说它所产生的constructor 是trivial。

(补充:类中拷贝构造函数和构造函数的区别)

构造函数与其他方法的区别:

1).构造函数的命名必须和类名完全相同,一般方法不能和类名相同。

2).构造函数的功能主要用于在类的对象创建时定义初始化的状态,它没有返回值,也不能用void。其他方法都有返回值,即使是void(尽管方法体本身不会自动返回什么,但仍然可以让它返回一些东西,而这些东西可能是不安全的)

3).构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用,一般方法在执行到它的时候才调用

4).当定义一个类定义的时候,通常显示其构造函数,在函数中的初始化工作也可省略,编译器会提供一个不带参数的默认的构造函数,一般方法不存在此特点。

拷贝构造函数:

拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。其作用是在建立一个新对象时,使用一个已经存在的对象去初始化这个新对象。例如 Point p(p1);

特点:

1)是一种构造函数,所以也没有返回值,函数名和类名相同。

2)只有一个参数,并且是同类对象的引用

3)每个类中必须有一个拷贝构造函数,可以自定义拷贝构造函数(深拷贝),如果没有自定义拷贝构造函数,系统会自动生成一个默认拷贝构造函数(浅拷贝)

调用的三种情况:

1) 当用类的一个对象去初始化该类的另一个对象。Point p(p1)或Point p=p1;

2)当函数的形参是类的对象时,调用函数进行形参和实参结合时,如:fun(Point p);

3)当函数的返回值是类的对象,函数执行完成返回调用者时。如:Point fun(){Point p;return p;}  main(){Point p2; p2=fun();}在函数体内,当执行到语句“return p”,将会调用拷贝构造函数将p的值复制到一个无名对象中,这个无名对象是编译系统在主程序中临时创建的。函数运行结束时对象p消失,但临时对象存在于“p2=fun();”语句中,执行完这行语句,临时无名对象的使命也就完成了,它会自动消失。)

三:浅拷贝和深拷贝

浅拷贝、深拷贝(参见http://blog.163.com/cocoa_20/blog/static/25396006200973174129609/)也可以书上P79的例子.

所谓浅拷贝,就是默认的拷贝构造函数所实现的对数据成员逐一赋值。若类中含有指针类型的数据,这种方法将会产生错误。为了解决浅拷贝出现的错误,必须显示地定义一个自己的拷贝构造函数(这样程序会调用自定义的拷贝构造函数),使之不但复制数据成员,而且为对象分配各自的内存空间。

四:虚函数和虚析构函数

1) 虚函数的作用:

可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时, 基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,而不是基类中定义的成 员函数(只要派生类改写了该成员函数)。
   若没有虚函数,则不管基类指针指向的哪个派生类对象,调用时都 会调用基类中定义的那个函数。(注:只需在基类的成员函数前加virtual关键字即可) 虚函数联系到多态,多态联系到继承。

虚指针或虚函数指针是一个虚函数的实现细节。带有虚函数的类中的每一个对象都有一个虚指针指向该类的虚函数表。

虚函数的入口地址和普通函数有什么不同?

解:

每个虚函数都在vtable(虚函数表)中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应的位置,调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了”动态连编“。而不是像普通函数那样简单地跳转到一个固定的地址。

虚函数详解:http://hi.baidu.com/bcsbyldlrabatxq/item/56a843a2e364bd12a8cfb7c0 (——1)

http://soft.chinabyte.com/database/428/11720928.shtml (——2)

2)虚析构函数:

例子:

#include<iostream>
using namespace std;
class Base{
public:
Base(){
}
virtual ~Base()
{
cout<<"Calling Base disconstruct\n";
}
void print()
{
cout<<"Base class\n";
}
}; class Child1:public Base{
public:
Child1(){}
~Child1()
{
cout<<"Calling Child1 disconstruct\n";
}
}; class Child1_2:public Child1{
public:
Child1_2(){}
~Child1_2()
{
cout<<"Calling Child1_2 disconstruct\n";
}
void f()
{
cout<<"Do something in child1_2\n";
}
}; int main()
{
//Child1_2 child2;
//child2.print();
// Child1_2 &p=child2;//这个表示的意思,是它的一个引用,也就是child2的一个别名。
//child2.~Child1_2();
// p.print();
Base *p=new Child1_2;
p->print(); delete p; return ;
}

C++中“类”相关知识点汇总

对上面的代码进行补充:

如果把virtual ~Base() 中的virtual去掉,程序将只执行基类Base的析构函数,而不会执行派生类的析构函数。原因是当撤销指针P所指的派生类的无名对象时,调用析构函数时,采用了静态连编方式,只调用了基类的析构函数。

如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。

补充解释:

我的理解是:“虚”是动态编译,“按对象匹配”,为什么呢?因为有“虚表指针”!

虚表指针的作用就是指向虚表,而虚表里有派生类“覆盖”重写的函数指针,这样,在编译阶段,VC只“按类型匹配”,而这时的新指针的类型是基类,于是“静 态编译”可以通过,当程序run时,又变成动态编译,所以才能“按对象匹配”。

虚析构虽然是“四大金刚”之一,但是毕竟它也只是个函数,所以它也适合“虚”的规则。其实,它就是个幌子,在程序结束时,先“按对象匹配”调用派生类的析构函数,再“按类型匹配”调用基类的析构函数。

PS:前面两行的解释,是为了迎合“析构是构造的逆序”这句话,实际情况只有问VC了。(他人言)

较好的解释:http://blog.csdn.net/starlee/article/details/619827

五:虚基类

如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最底层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在同名成员前增加直接基类名,使其唯一地标识一个成员,以免产生二义性,如下:

#include<iostream>
using namespace std;
class Base{
public:
Base() //在类中直接实现其构造函数为隐式调用内联函数,补充:析构函数可以显示调用内联函数
{
a=;
cout<<"Base a="<<a<<endl;
}
protected:
int a;
}; class Base1:public Base{
public:
Base1()
{
a=a+;
cout<<"Base1 a="<<a<<endl;
}
}; class Base2:public Base{
public:
Base2()
{
a=a+;
cout<<"Base2 a="<<a<<endl;
}
};
class Derived:public Base1,public Base2{//Derived是Base1和Base2的共同派生类,是Base的间接派生类
public:
Derived()
{
cout<<"Base1 a="<<Base1::a<<endl;
cout<<"Base2 a="<<Base2::a<<endl;
}
};
int main()
{
Derived obj;
return ;
}

C++中“类”相关知识点汇总

如果在上述的程序中的代码

Derived()

{

cout<<"Base1 a="<<Base1::a<<endl;

cout<<"Base2 a="<<Base2::a<<endl;

}中的cout的语句改成:cout<<"Derived a="<<a<<endl;将会产生二义性,a的值可能是从Base1派生路径来的,也可能是从Base2派生路径来的,为了解决这种二义性,C++引入了“虚基类”的概念

在上例中,如果类Base只存在一个拷贝,那么对a的访问就不会产生二义性。C++中通过将这个公共基类声明为虚基类来解决这个问题,声明形式为:

class 派生类名:virtual 继承方式 类名{...}

将上面代码改一下:

#include<iostream>
using namespace std;
class Base{
public:
Base() //在类中直接实现其构造函数为隐式调用内联函数,补充:析构函数可以显示调用内联函数
{
a=;
cout<<"Base a="<<a<<endl;
}
protected:
int a;
}; class Base1:virtual public Base{ //声明类Base是类Base1的虚基类
public:
Base1()
{
a=a+;
cout<<"Base1 a="<<a<<endl;
}
}; class Base2:virtual public Base{//声明类Base是类Base2的虚基类
public:
Base2()
{
a=a+;
cout<<"Base2 a="<<a<<endl;
}
};
class Derived:public Base1,public Base2{//Derived是Base1和Base2的共同派生类,是Base的间接派生类
public:
Derived()
{
cout<<"Derived a="<<a<<endl;
}
};
int main()
{
Derived obj;
return ;
}

C++中“类”相关知识点汇总

这样就消除了二义性。(说明:virtual关键字和继承方式的先后顺序是无关紧要的。)

使用虚基类机制要注意一下几点:

1)如果虚基类中定义有带形参的构造函数,并且没有默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。

2)建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员时由最远派生类的构造函数通过虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都自动被忽略。

3)若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。

4)对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。

5)对于非虚基类,构造函数的执行顺序仍是先左后右,自上而下。

6)若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。

#include<iostream>
using namespace std;
class Base{
public:
Base(int sa) //在类中直接实现其构造函数为隐式调用内联函数,补充:析构函数可以显示调用内联函数
{
a=sa;
cout<<"Construct base a="<<a<<endl;
}
protected:
int a;
}; class Base1:virtual public Base{
public:
Base1(int sa,int sb):Base(sa)
{
b=sb;
cout<<"construct base1 b="<<b<<endl;
}
private:
int b;
}; class Base2:virtual public Base{
public:
Base2(int sa,int sc):Base(sa)
{
c=sc;
cout<<"construct base2 c="<<c<<endl;
}
private:
int c;
};
class Derived:public Base1,public Base2{//Derived是Base1和Base2的共同派生类,是Base的间接派生类
public:
Derived(int sa,int sb,int sc,int sd):Base(sa),Base1(sa,sb),Base2(sa,sc)
{
d=sd;
cout<<"Derived d="<<d<<endl;
}
private:
int d;
};
int main()
{
Derived obj(,,,);
return ;
}

C++中“类”相关知识点汇总

在上述程序中,类Base是一个虚基类,它只有一个带参数的构造函数,因此要求在派生类的构造函数的初始化列表中,都必须带有对类Base构造函数的调用。

如果类Base不是虚基类,在派生类Derived的构造函数的初始化列表中调用类Base的构造函数是错误的,由程序运行结果可知,当类Derived的构造函数调用了虚基类Base的构造函数之后,类Base1和类Base2对类Base的构造函数的调用就被忽略了。