- C++面向对象如何理解:
将具有相似特征的实体集合起来形成类;将类实例化形成对象;面向对象,使计算机软件系统与现实世界系统一一对应起来。
- C++的三大特性:封装、继承、多态
- 类对象所占空间
- http://www.cnblogs.com/weiyouqing/p/9642986.html
- 空类必须占一个字节;
- 函数指针不占字节;
- 虚函数根据编译器位数,占相应字节;系统多用了一个指针维护这个类的虚函数表,因此虚函数占用的内存就是指针占用的内存
子类继承父类,会继承了这个虚函数表,占用4字节。
- 类具有4字节对齐功能;类的内存大小始终为4字节的倍数
- 类中的静态成员变量不占类的内存(不属于类的实例,储存在静态区);并且静态成员变量的初始化必须在类外初始化;
- 子类在内存中占的字节数为父类所占字节数+自身成员所占的字节数;
- class E:virtual public A,virtual public B{}; ->(8),一个虚继承需要一个指向父类的指针。(见文章末尾有详细解答)
注意虚拟继承和虚函数在类中内存分配的区别,类B虚拟继承类A,类C继承类B,此时,类B会有虚拟继承的指针,类C内存中会有类B的指针,但不会重新生成一个属于类C的虚拟继承的指针。
而虚拟继承,类A中有一个虚函数,而类B继承类A,此时,类A会有虚函数表,而类B也会有虚函数表。
- class 和struct 的区别
- C struct和C++ class的区别:
struct只是作为一种复杂数据类型定义,不能再struct中定义成员函数,而class则是面向对象的。
- C++ struct和C++ class的区别:
C++ struct扩充了C struct的功能,可以面向对象。
struct默认是public成员访问权限以及public继承,而class则是private成员访问权限以及private继承。
在C++模板中,class可以用于模板类型而struct不行。
- 类成员初始化顺序 -> 与声明顺序一致
class A { public: A:b(2),a(b){}//先初始化a(b值未知,因此a随机数),在初始化b(2) private: int a;//先声明a,在声明b int b; };
- 构造函数初始化列表和构造函数体
- 对于常成员变量和引用类型成员变量就一定要使用初始化列表。
class A { public: A():a(1),b(2){} private: const int a; int &b; };
- 子类初始化父类,倘若父类中的成员变量为私有,则子类不可调用,因此只能在初始化列表中调用父类的构造函数。
- 空类会产生哪些默认的成员函数
class Empty { public: Empty(); // 缺省构造函数 Empty( const Empty& ); // 拷贝构造函数 ~Empty(); // 析构函数 Empty& operator=( const Empty& ); // 赋值运算符 Empty* operator&(); // 取址运算符 const Empty* operator&() const; // 取址运算符 const };
- implicit explicit作用
- C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式);如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。
- google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。
- effective c++中说:被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit,鼓励大家遵循相同的政策。
class CxString // 没有使用explicit关键字的类声明, 即默认为隐式声明 { public: char *_pstr; int _size; explicit CxString(int size) { _size = size; // string的预设大小 _pstr = malloc(size + 1); // 分配string的内存 memset(_pstr, 0, size + 1); } }; int main() { CxString string1(24); //显式调用 CxString string2 = 10; //隐式调用失败,因为有explicit return 0; }
- 父类指针指向子类
子类总是含有一些父类没有的成员变量,或者方法函数。而子类肯定含有父类所有的成员变量和方法函数。所以用父类指针指向子类时,没有问题,因为父类有的,子类都有,不会出现非法访问问题。
但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法,因为被子类指针指向的由父类创建的对象,根本没有要访问的那些内容,那些是子类特有的,只有用子类初始化对象时才会有。
- 为什么要用父类的指针指向子类(Base* point=new Child)
- 为了实现多态,假设有一个父类Base,三个子类A,B,C,子类都含有父类的相关特性,假设父类有50个特性(成员变量和成员函数),那子类继承父类后便可获得这50个特性,其中假设父类中是虚函数的话,子类便可去重新修改此函数,达到多态的目的,即同一操作作用于不同的对象有不同的解释。这样,我们就不必让A,B,C每个类都重新去写那50个特性,方便快捷很多。
- 将父类比喻为电脑的外设接口,子类比喻为外设,现在我有移动硬盘、U盘以及MP3,它们3个都是可以作为存储但是也各不相同。如果我在写驱动的时候,我用个父类表示外设接口(包含一些接口特定的特性),然后在子类中重写父类那个读取设备的虚函数,那这样电脑的外设接口只需要一个。但如果我不是这样做,而是用每个子类表示一个外设接口,那么我的电脑就必须有3个接口分别来读取移动硬盘、U盘以及MP3。若以后我还有SD卡读卡器,那我岂不是要将电脑拆了,焊个SD卡读卡器的接口上去?所以,用父类的指针指向子类,是为了面向接口编程。大家都遵循这个接口,弄成一样的,到哪里都可以用,准确说就是“一个接口,多种实现“。
- 虚函数表
- 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
- 一般继承(无虚函数)
一般继承(有虚函数覆盖)
多重继承(无虚函数覆盖)
- 多重继承(有虚函数覆盖)
-
C++虚表地址和虚函数地址
#include <iostream> using namespace std; typedef void(*Fun)(void); //函数指针 class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } void h() { cout << "Base::h" << endl; } int i = 10; }; int main() { Base b; //输出类b地址 cout << &b << endl;//类b的地址 //cout << *(&b) << endl;错误! //输出类b地址,但此时(int*)&b为整型指针 cout << (int*)&b << endl; //输出虚函数表地址的十进制 cout << *(int*)&b << endl; //输出虚函数表地址 cout << (int*)*(int*)&b << endl; //输出虚函数表中第一个虚函数的地址的十进制 cout << *(int*)*(int*)&b << endl; //输出虚函数表中第一个虚函数的地址 cout << (int*)*(int*)*(int*)&b << endl; //输出虚函数表中第二个虚函数的地址的十进制 cout << *((int*)*(int*)&b+1) << endl; //输出虚函数表中第二个虚函数的地址 cout << (int*) *((int*)*(int*)&b + 1) << endl; Fun pfun = (Fun)*((int *)*(int *)(&b)); //vitural f(); printf("f():%p\n", pfun); pfun(); pfun = (Fun)*((int *)*(int *)(&b)+1); //vitural g(); printf("g():%p\n", pfun); pfun(); system("pause"); return 0; }
- 查看类b内存
- //cout << *(&b) << endl;出错
//不可以这样操作,因为&b是类b的地址,*(&b)意思是取类b,而没有重载运算符实现输出类b,因此出错
- cout << "强制转换成整型指针的类b地址 " << (int*)&b << endl;
//将类b地址强制转换成整型指针,即此时(int*)&b代表的是一个整型变量的地址,内存为4字节,而不是代表一个类b的地址,因此可以操作*(int*)&b
- cout << "虚函数表地址的十进制 " << *(int*)&b << endl;
首先你先把&b强制转换成整型指针了,即(int*)(&b),就好比int i=0;int* a=i;一样,cout<<*a(此时将a类比成(int*)(&b))的时候输出为0。因此*(int*)(&b)输出的是整型也就是十进制,这跟C++ 输出运算符的重载有关,他检测到你是整形指针,自然输出整型,若使用C语言的printf("%p\n", *(int*)(&b))就不会出现十进制,因为已经在输出的时候强制转换成指针,因此是输出地址。
取类b地址上的前四字节的东西,那就是虚函数表的地址,只不过是将其转化为十进制而已。
- cout << "虚函数表地址 " << (int*)*(int*)&b << endl;
而(int*)*(int*)(&b)则是在*(int*)(&b)的基础上将其转换成整型指针,自然输出的是十六进制,代表虚函数表的地址。
- cout << "输出虚函数表中第一个虚函数的地址的十进制 " << *(int*)*(int*)&b << endl;
同理,取虚函数表起始地址上的东西,即是第一个虚函数,输出其地址的十进制
- cout << "输出虚函数表中第二个虚函数的地址的十进制 " << *((int*)*(int*)&b + 1) << endl;
此时注意(int*)*(int*)&b代表的是虚函数表的起始地址,而虚函数表起始地址加一,则是第二个虚函数的地址
这里注意,起始地址所对应的内容占4字节,起始地址加一后,则内存会在原先基础上往后偏移4字节。
- 接下来则是定义函数指针,指向第一个以及第二个虚函数,验证正确与否。
- 虚析构函数
为实现多态,用父类的指针指向子类的时候,当程序结束时,若析构函数没有设置成虚函数,则发现只析构了父类函数,没有析构子类函数,造成内存泄露。
所有基类的析构函数,都应该声明为虚析构函数!这也是c++标准所指定的。
无论是用父类的指针指向新生成的堆中的子类亦或是用子类的指针指向新生成的堆中的子类,都会调用父类的构造函数!
- 复制构造函数
- 什么是复制构造函数,复制构造函数和赋值运算符的区别
拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。二者的区别就是主要看是否有新的对象实例产生。如果类中没有显示的定义一个复制构造函数,编译器会私下为类定义一个构造赋值函数。
- 编译器哪时会自动生成复制构造函数?
倘若自己已经写了一个构造函数,则编译器则不会生成;倘若没写,但用了复制构造函数,编译器则会生成,但如果没用,编译器则不会生成。
- 调用拷贝构造函数主要有以下场景
对象作为函数的参数,以值传递的方式传给函数。
对象作为函数的返回值,以值的方式从函数返回(函数返回后局部变量就被销毁了,但返回时在生成了临时变量作为函数的返回值。因此用复制构造函数构造了临时对象)
使用一个对象给另一个对象初始化
#include <iostream> using namespace std; class Complex { private: double m_real; double m_imag; public: Complex(const double real = 0, const double imag = 0) { m_real = real; m_imag = imag; cout << "construct" << endl; } ~Complex() { cout << "destory" << endl; }; Complex(const Complex & c) { m_real = c.m_real; m_imag = c.m_imag; cout << "copy" << endl; } Complex &operator=(const Complex&tem) { m_real = tem.m_real; m_imag = tem.m_imag; cout << "operator =" << endl; return *this; } }; Complex fun1() { Complex Test(2, 3); return Test; } Complex fun2(Complex C) { return C; } int main() { Complex c1 = fun1(); Complex c2 = fun2(c1); c2 = fun2(c1); return 0; }
- Complex c1 = fun1();:生成Test临时对象(construct)->将Test复制给c1(copy)->析构函数析构Test(destory)
- Complex c2 = fun2(c1);:将c1复制给C,生成临时对象(copy)->将C复制给c2(copy)->析构函数析构C(destory)
- c2 = fun2(c1);:将c1复制给C,生成fun2中的临时对象C(copy)->将C复制给main中的临时对象(copy)->析构函数析构C(destory)->将临时对象用赋值运算符赋给c2(operator =)->将main中的临时对象析构(destory)
之所以Complex c2 = fun2(c1);和c2 = fun2(c1);会产生差别,就是因为第一个是生成一个新的对象的实例,直接用复制构造函数,将fun2的临时变量复制给c2,而第二个则是将对象的值复制给一个已经存在的实例,则需要将c2临时变量复制给在main上的临时变量,才可以进行赋值操作。
- 析构c2(destory)->析构c1(destory)
- 深复制浅复制
- 浅复制:复制这个对象的时候,让新旧两个对象指针指向同一个外部的内容。
b1=a1执行的是浅复制,此时a1.a和b1.a指向的是同一个内存地址,如果在析构函数里面有对内存的释放。就会出现内存访问异常。因为一块内存空间会被释放两次,程序崩溃。
class A{ public: int* a; }; int main() { A a1; A b1=a1;//编译器自动补上的浅复制的复制构造函数 }
- 深复制:复制这个对象时,为新对象制作了外部对象的独立复制,不仅仅是只有一个内容,两个指向。
person(const person &p)//深拷贝构造函数 { name=new char[strlen(p.name)+1]; strcpy(name,p.name); cout<<"copy construct ..."<<endl; } ~person() { if(name!=NULL){ delete name; name=NULL; } cout<<"destruct ..."<<endl; }
- 复制构造函数以及深复制,如何在继承中写构造函数(运用复制构造函数),构造析构顺序,子类重载=运算符运用
#include <iostream> using namespace std; #include <fstream> fstream fout("destructor.txt", ios::app); #pragma warning(disable:4996) class Base { public: Base(const int tem_i = 0, const char *tem_str = NULL) :i(tem_i) { if (tem_str != NULL) { str = new char[strlen(tem_str) + 1]; strcpy(str, tem_str); } cout << str << " " << "Base constructor" << endl; } Base(const Base &tem) { i = tem.i; str = new char[strlen(tem.str) + 1]; strcpy(str, tem.str); cout << str << " " << "Base copy" << endl; } ~Base() { cout << str << " " << "Base destory" << endl; if (str != NULL) { delete str; str = NULL; } } Base &operator=(const Base&tem) { if (str != NULL) delete str; i = tem.i; str = new char[strlen(tem.str) + 1]; strcpy(str, tem.str); cout << str << " " << "Base operator =" << endl; return *this; } private: int i; char *str; }; class Child :public Base { public: Child(const int tem_i = 0, const char *tem_str = NULL, const int tem_j = 0) :Base(tem_i, tem_str), j(tem_j) { cout << "Child constructor" << endl; } Child(const Child &tem) :Base(tem) { j = tem.j; cout << "Child copy" << endl; } Child &operator=(const Child&tem) { Base::operator=(static_cast<Base>(tem));//强制转换成Base类型 j = tem.j; cout << "Child operator =" << endl; return *this; } ~Child() { cout << "Child destory" << endl; } private: int j; }; int main() { Base a(10,"Hello0"); Child a1(7, "Hello1", 50); Base b(10, "Hel"); Child b1(a1); Child c1(5, "Hello2", 70); a = b; c1 = a1; system("pause"); return 0; }
- str=new char[int]和str=new char(int)是不等同的,千万注意别马虎出错了。
str=new char[int]表示申请内存大小为多少字节的char;而str=new char(int)则是申请一个字节大小的char,char为char型的int。
若不小心写错了,就会出现以下错误。
- 关于delete和delete []
在这个函数中,我使用了str = new char[strlen(tem.str) + 1];但只使用了delete str;通过内存调试来看,这样是ok的。因为这是简单类型,像int/char/long/int*/struct等等简单数据类型,分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,因此delete str 和delete []str是等效。
而在自定义类型中,比如自定义一个类的时候,A* p= new A[3];delete p;这样才只调用了p[0]的析构函数,造成了内存泄露,而使用了delete[]则调用了3个Babe对象的析构函数。
- 由于父类的成员变量i是私有变量,因此子类的复制构造函数不能直接用父类的私有变量去赋值,因此需要调用父类的构造函数。
- 基类重载=运算符:Base &operator=(const Base&tem)
因为重载=运算符是不生成新类,也就是说原本已有Base这个类的实例,因此记得将其的str delete才再次申请一个新的str堆内存。
声明=运算符时之所以要用到引用,是因为如果用”值传递“的方式,虽然功能仍然正确,但由于return语句要把*this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,会降低赋值函数的效率。
末尾用return *this,是因为要返回一个类,因此使用了*this。
- 子类重载=运算符:Child &operator=(const Child&tem)
子类重载运算符要考虑基类的重载问题,这里提供了一种解决办法,Base::operator=(static_cast<Base>(tem));,先将tem从子类强制转换成基类,而后调用基类的重载。注意分成以下4步骤:
- 调用基类复制函数,生成一个临时对象,用以保存子类转换成base类型的类实例。
- 将上述生成的临时类实例用以基类的运算符重载,将临时对象的str赋予this
- 基类运算符重载后,将临时类实例析构。
- 子类进行运算符重载。
- 各种运算符的重载
- 重载<<学会用getline(*str,count);
- "aaa"结束符为'\0',而不是'\n';'\n'的意思是回车
- 重载运算符的时候,括号里面的数代表位于运算符后方的变量,若是单目运算符,则括号里一般不写东西,因为后方无东西,且重载函数里面一般调用了其*this指针因此括号不需要加内容;而重载<<或>>运算符的括号则需要加(A,B),分别代表<<或>>的前后。
- .h文件
#ifndef _POINT #define _POINT #include<iostream> using namespace std; class Str { public: Str(const char*tem = NULL); Str(const Str&tem); ~Str(); Str& operator=(const char*tem); Str& operator=(const Str&tem); Str& operator+=(const char*tem); Str& operator+=(const Str&tem); bool operator<(const Str&tem); bool operator>(const Str&tem); bool operator==(const Str&tem); bool operator!=(const Str&tem); friend ostream& operator<<(ostream& out, const Str& tem); friend istream& operator>>(istream& in, const Str& tem); char* Getchar(); private: char *str; }; #endif
- .cpp文件
#pragma warning(disable:4996)//防止strcpy报错 #include "point.h" #include <iostream> using namespace std; Str::Str(const char * tem) { if (tem == NULL) str = new char('\0'); else { str = new char[strlen(tem) + 1]; strcpy(str, tem); } } Str::Str(const Str & tem) { str = new char[strlen(tem.str) + 1]; strcpy(str, tem.str); } Str::~Str() { delete str; str = NULL; } Str & Str::operator=(const char * tem) { delete str; str = NULL; if (tem == NULL) str = new char('\0'); else { str = new char[strlen(tem) + 1]; strcpy(str, tem); } return *this; } Str & Str::operator=(const Str & tem) { if (&tem == this) return *this; delete str; str = NULL; if (tem.str == NULL) str = new char('\0'); else { str = new char[strlen(tem.str) + 1]; strcpy(str, tem.str); } return *this; } Str & Str::operator+=(const char * tem) { char* tem_str = str; if (tem == NULL) return *this; else { str = new char[strlen(tem) + strlen(tem_str) + 1]; strcpy(str, tem_str); delete tem_str; strcat(str, tem); } return *this; } Str & Str::operator+=(const Str & tem) { char* tem_str = str; if (tem.str == NULL) return *this; else { str = new char[strlen(tem.str) + strlen(tem_str) + 1]; strcpy(str, tem_str); delete tem_str; strcat(str, tem.str); } return *this; } bool Str::operator<(const Str & tem) { //没有对tem.str==NULL做分析 int i = 0; while (str[i] != '\0' && tem.str[i] != '\0') {//当任一字符串没到结束符前的对比 if (str[i] < tem.str[i]) return true; else if (str[i] > tem.str[i]) return false; i++; } //当任一字符串到结束符时对比还没对比出结果,说明之前的都相等 if (str[i] == '\0' && tem.str[i] == '\0')//如果同时到达结束符,说明相等 return false; else if (str[i] == '\0')//如果先到达结束符,说明小,另一个的字符长度长一点。 return true; else return false; } bool Str::operator>(const Str & tem)//类比与< { int i = 0; while (str[i] != '\0' && tem.str[i] != '\0') { if (str[i] > tem.str[i]) return true; else if (str[i] < tem.str[i]) return false; i++; } if (str[i] == '\0' && tem.str[i] == '\0') return false; else if (str[i] == '\0') return false; else return true; } bool Str::operator==(const Str & tem) { int i = 0; for (int i = 0;;i++) { if (str[i] == '\0' && tem.str[i] == '\0')//同时到达结束符,则说明长度相等 return true; else if (str[i] != tem.str[i])//若有任何一处不同,则不等 return false; } } bool Str::operator!=(const Str & tem)//类比== { int i = 0; for (int i = 0;;i++) { if (str[i] == '\0' && tem.str[i] == '\0') return false; else if (str[i] != tem.str[i]) return true; } } char * Str::Getchar() { return str; } ostream & operator<<(ostream & out, const Str & tem) { int i = 0; while (tem.str[i] != '\0') { out << tem.str[i]; i++; } out << endl; return out; } istream & operator>>(istream & in, Str & tem) { char tem_str[50]; in.getline(tem_str, 50); tem = tem_str; return in; } int main() { Str s("aaa"); Str s1(s); Str s2("asdf"); Str s3; cout << "s: " << s; cout << "s1: " << s1; cout << "s2: " << s2; cout << "s3: " << s3; s3 = s2; cout << "s3: " << s3; s3 = "12ab"; cout << "s3: " << s3; s3 += "111"; cout << "s3: " << s3; s3 += s1; cout << "s3: " << s3; cin >> s1; cout << "s1: " << s1; Str t1 = "1234"; Str t2 = "1234"; Str t3 = "12345"; Str t4 = "12345"; cout << (t1 == t2) << endl; cout << (t1 < t3) << endl; cout << (t1 > t4) << endl; cout << (t1 != t4) << endl; system("pause"); return 0; }
- 友元(friend)
- 优缺点:友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
- 友元函数:
- 友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明(friend),因此声明放在private和public都可以。
- 一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
- 为什么 friend istream& operator>>(istream& in, const Str& tem); 要定义为友元函数
我们可以看到在上面重载的程序中的cpp中,单目运算符的重载的书写是Str & Str::operator+=(const char * tem)(注意:有个Str::,这就说明了此函数里面蕴含一个this指针指向该类,而且说明这个函数是类的成员函数,因此可以读取类的私有变量),除此之外,单目运算符一般只有一个参数,例如重载+,即系统会自动更改为obj.operator+(...),这样的话也可以说明重载+可以调用其私有函数;
而重载输入输出运算符,由于我们的习惯是先写cout<<" ";也就是先写ostream类的东西,这样导致了在声明时就要这样:friend ostream& operator<<(ostream& out, const Str& tem); 既不是单目运算符,而且第一个参数也并不是类的实例,不是成员函数,不含有this指针,因此要用到friend才可使重载访问私有变量。
倘若不想用户friend,第一种方法可以抛弃习惯,但不推荐。
//.h ostream& operator<<(ostream& out); //.cpp ostream & Str::operator<<(ostream & out) { int i = 0; while (str[i] != '\0') { out << str[i]; i++; } out << endl; return out; } //main Str s2("asdf"); s2 << cout;
第二种方法,将重载<<写成普通函数,但有接口去实现对私有变量的访问。
//在类中定义私有变量的接口 void setReal(int parm){ real = parm;} void setImag(int parm){ imag = parm;} //将其作为普通函数使用 ostream& operator << (ostream& cout, complex& par); { cout << par.getReal() << " + " << par.getImag() << "i" << endl; return cout; }
//类中的声明要加friend friend ostream& operator<<(ostream& out, const Str& tem); //定义则不需要friend,注意与inline区分, //inline是类中的函数自动归为inline,类外定义的函数想要转成内联函数需要inline ostream & operator<<(ostream & out, const Str & tem) { /......../ return out; }
- 友元类
- 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
- 只有在类A中声明了类B是它的友元类,这样B才是A的友元类,其余的都不算是,没有继承性没有传递性没有交换性等等。
class A { public: friend class B; };
- 继承
- 派生类可以获得基类的成员数据和方法。
- 公有、保护、私有成员
- 公有成员:公有成员能被继承,也可以被外部函数访问。
- 保护成员:能够被继承,但是不能被外部函数访问。(也就是说,可以被派生类的成员函数引用)
- 私有成员:私有成员不能被继承,也不能被外部函数访问。
- 公有、保护、私有继承
- 多态
- 什么是多态?
多态的意思就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
- 多态有几种?
- 编译时的多态:通过重载来实现。
- 运行时的多态:直到系统运行时,才根据实际情况决定实现何种操作。C++中,运行时的多态是通过虚函数实现。
- 虚函数在传参时引用与否的区别以及一些注意事项
#include<iostream> using namespace std; class A { public: virtual void print(); virtual void f(int i = 1); }; class B :public A { public: virtual void print(); virtual void f(int i = 2); }; void A::print() { cout << "A" << endl; } void A::f(int i) { cout << "A::f() " << i << endl; } void B::print() { cout << "B" << endl; } void B::f(int i) { cout << "B::f() " << i << endl; } void print1(A &tem) { tem.print(); } void print2(A tem) { tem.print(); } void main() { A a; B b; print1(a);//输出A print1(b);//输出B print2(a);//输出A print2(b);//输出A A *c = new B ; c->f();//输出B::f() 1 system("pause"); }
- 引用(print1(A &tem))
将对象直接引用到tem,即调用的函数是a.tem以及b.tem。
- 直接传参(print2(A tem))
在函数栈中生成类A的临时对象,因此执行的都是A的成员函数。
- 为什么c->f()是输出B::f() 1而不是B::f() 2
记住参数的值是在编译器就已经决定的,而不是在运行期,因此i应该取基类的默认值,即1。
- 虚拟继承
- 借鉴https://blog.csdn.net/xiaozuo666/article/details/80373316,写得真心不错
- 直接继承与菱形继承的内存分配
- 二义性
编译器对D::_b的访问不明确。这是当然的,以为在派生类对象
d
中存在两个int _b
成员(分别从C1,C2继承下来的),因此在对d._b
进行赋值时,编译器不知道访问从C1继承下来的,还是从C2继承下来的,因此对_b
访问不明确,这就是菱形继承存在的二义性。int main() { D d; d._c1 = 1;//改成“d.C1::_b = 1;”即可 d._b = 2; d._c2 = 3;//改成“d.C2::_b = 3;”即可 d._b = 4; d._d = 5; return 0; }
类的内存展示:
- 虚拟直接继承
class B { public: int _b; }; class D :virtual public B { public: int _d; }; int main() { B b; D d; cout << sizeof(b) << endl;//输出4 cout << sizeof(D) << endl;//输出12 d._d = 1; d._b = 2; return 0; }
派生类多出来的4个字节是指针,指针指向的内容前四字节是0,意思是指针地址对于派生类地址的偏移量,因为派生类地址的一开始就是指针地址,因此指针地址也就是派生类地址,因此为0;指针指向的内容前四字节是x,意思是指针地址对于派生类内存中,基类变量地址的偏移量。
下图可以说明上述说明,首先派生类的地址为&d=001FFEAC;其前四字节为指针,内容为00FA7B30,而00FA7B30地址指向的内容为8字节,分别为0和8;接下来就是派生类中自己的成员变量_d的内存存放,即为1;而后是父类的变量存放,其存放在汇编代码如下:
mov eax,dword ptr [d] ;double word 双字节*2=4字节 mov ecx,dword ptr [eax+4] mov dword ptr d[ecx],2
- 先把类d的地址上的4字节内容存放给eax,即001FFEAC上的00FA7B30放置在寄存器eax上
- 将寄存器eax的地址偏移4后作为新地址,将此地址上的四字节内容放置于ecx中,即8放置于ecx
- 将2放置在d地址偏移(ecx)位后的地址上,即d地址为001FFEAC,偏移8位后,即001FFEB4,将2放置于001FFEB4
- 虚拟菱形继承
class B { public: int _b; }; class C1 :virtual public B { public: int _c1; }; class C2 :virtual public B { public: int _c2; }; class D :public C1,public C2 { public: int _d; }; int main() { D d; cout << sizeof(B) << " " << sizeof(C1) << " " \ << sizeof(C2) << " " << sizeof(D) << " " << endl; //输出4 12 12 24 d._c1 = 1; d._c2 = 2; d._d = 3; d._b = 4; return 0; }
- 虚拟菱形继承(D)调用构造函数顺序
- 先生成类C1:先调用虚拟基类(B)的默认构造函数(系统碰到多重继承的时候会自动先加入一个虚拟基类的拷贝,只生成一份拷贝),而后调用C1的构造函数
- 生成类C2:由于已经生成了(B)的构造函数,因此便不在调用B的构造函数,只调用C2的构造函数
- 生成类D,调用类D的构造函数
- 私有继承和组合的区别
- 什么是组合(类比在链表中的操作)
template<class T> class IntSLLNode { }; template<class T> class IntSLLList { IntSLLNode<T> tail; };
在类List中把类Node当成是自己的成员变量
- 区别
- 私有继承派生类可以访问父类的protected成员,但组合就不行,例如List就不可访问tail的protected成员,因为属于类外访问。
- 如果基类的抽象类,拥有纯虚函数,那么不可以使用组合,因为抽象类不能被实例化。
- 纯虚函数以及抽象类
- 纯虚函数
virtual void fun()=0;
纯虚函数在基类中是没有定义的,必须到子类中实现,类似于接口函数。
- 抽象类
含有一个或多个的类即为抽象类,抽象类的存在是因为有时候一个基类是很抽象的,很难去将一个类去实例化,因为它仅仅只是定义了接口,把派生类的共同行为提取出来。
- 虚函数
虚函数也有空实现,但虚函数是既继承了接口,也继承了父类的实现,不像纯虚函数,实现一定要由子类完成。