类
C++使用struct、class来定义一个类:struct的默认成员权限是public,class的默认成员权限是private;除此之外,二者基本无差别。类的成员函数以及友元函数可以访问类中的所有成员,类外通过类的对象只能访问公有成员。
1 2 3 4 5 6 7 8 9 class Student {private : string name; double score; public : double GetSorce () { return score; } };
上面学生的类并不是真实世界中的学生,只是一个抽象的概念,并不包含真实世界中学生的所有属性,只是把一些属性抽象出来。
成员函数
类的内部,声明成员函数是必须的,但定义成员函数是可选的。类体内部定义的函数默认为inline。
调用成员函数时,实际上是使用对象来调用的。每个成员函数(除了static成员函数)都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象地址。程序员在成员函数中处理非静态数据成员时,隐式的类对象就会发生:
1 2 3 4 5 6 7 8 9 10 void rectangle::SetLength (double a) { len += a; } void rectangle::SetLength (rectangle* const this , double a) { this -> len += a; }
构造函数
构造函数是特殊的成员函数,和类同名且没有返回类型。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。
若没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认的构造函数,默认构造函数不带参数。若编译器自动生成默认构造,或者程序员自己定义了一个未进行任何操作的默认构造,则类中的每个成员使用与初始化变量相同的规则来进行初始化:1)类成员运行该类的默认构造函数来初始化;2)内置类型或者符合类型的成员初始值依赖作用域,局部作用域中这些成员不被初始化、全局作用域中它们被初始化为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class student {public : Student (); void show () ; private : string name; int number; int score; }; Student a; int main () { student b; }
某些情况下,默认构造函数是被编译器隐式应用的,如果程序员定义了其他构造函数,编译器将不合成默认的构造函数,如果我们不定义默认的构造函数,该类的使用将受限。假设一个NoDefault的类定义了一个有string实参的构造函数,却不定义默认构造,那么:
所有包含NoDefault对象成员的类的构造函数,必须在成员初始化列表中通过传递一个初始的string给Nodefault构造函数来显式的初始化Nodefault成员;
编译器不再为含有NoDefault类型成员的类合成默认构造函数,程序员必须显式定义默认构造,且默认构造中必须显式的初始化其NoDefault成员;
NoDefault类型不能用作动态分配数组的元素类型;
NoDefault类型的静态分配数组必须为每一个元素提供一个显式的初始化式;
如果一个容器比如vector保存NoDefault对象,那么就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数;
成员初始化列表
构造函数后面可以接初始化列表,为类的一个或多个数据成员指定初值。需要注意,C++中成员变量的初始化顺序与变量在类型中声明的顺序相同,而和它们在初始化列表中的顺序无关!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { private : int i; int j; public : A ():j (0 ),i (j+2 ){}; void print () { cout << i << j << endl; } };
可以不使用初始化列表,而是在构造函数中为成员赋值,不过这样做效率会有点低。从概念上讲,构造函数的执行阶段分为两步:第一步是初始化阶段;第二步是计算赋值阶段。普通的内置类型成员由于不进行隐式初始化,所以无论使用初始化列表还是在构造函数中赋值都无关紧要,但如果是一个类成员,不在初始化列表中进行显式初始化,而是在构造函数函数体里赋值,相当于先调用这个类成员自身的构造函数进行初始化,然后再走赋值赋值操作,多了一个步骤。
没有默认构造函数的类类型的成员变量、const类型的成员变量、引用类型的成员变量必须在构造函数初始化列表中进行初始化。
复制(拷贝)构造函数
复制构造函数、赋值操作符和析构函数总称为复制控制,编译器会自动实现操作,不过程序员可以定义自己的版本。一个有用的经验:如果一个类需要析构函数,那么它也同时需要赋值操作符和复制构造函数。通常编译器自己合成的复制构造函数非常精炼,只做必需的工作。
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数或拷贝构造函数。复制构造函数的作用:
根据另一个同类型的对象初始化一个对象;
复制一个对象,将它作为实参传给一个函数或从函数返回时复制一个对象;
初始化顺序容器中的元素;
根据元素初始化式列表初始化数组元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 string dots (10 , '.' ) ; string dots2 = "." ; string dots3 (dots) ; string dot4 = dots; string null_book = "9-999-9999-9" ; string null_book2; null_book2 = null_book;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class MyClass {public : MyClass (int n) { number = n; } MyClass (const MyClass& other){ number = other.number; cout << "a " ; } private : int number; }; void fun (MyClass p) { MyClass temp (p) ; } int main () { MyClass obj1 (10 ) , obj2 (0 ) ; MyClass obj3 (obj1) ; fun (obj3); return 0 ; }
1 2 3 4 5 vector<string> svec (5 ) ;
1 2 3 4 5 6 7 8 9 10 Sales_item_primer_eds[] = { string ("0-201-16487-6" ), string ("0-201-54848-7" ), string ("0-201-82470-1" ) }
复制构造函数参数必须是引用,如果不用引用,那就变成了值传递。值传递方式会调用该类的拷贝构造函数,从而造成无穷递归的调用拷贝构造。
深拷贝、浅拷贝
浅拷贝:只拷贝指针地址,对所有对原对象的引用仍然指向原对象,C++默认拷贝构造函数与赋值运算符重载都是浅拷贝;节省空间,但容易引发多次释放;
深拷贝:重新分配堆内存,拷贝指针指向内容,且引用原对象的变量将指向被复制的新对象;浪费空间,但是不会导致多次释放;
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Test { char * ptr; } void shallow_copy (Test& src, Test& dest) { dest.ptr = src.ptr; } void deep_copy (Test& src, Test& dest) { dest.ptr = malloc (strlen (src.ptr) + 1 ); memcpy (dest.ptr, src.ptr); }
深拷贝的思想比较常见,比如C++的一个优化策略叫写时复制 。有个信息存放在内存空间中,如果大家都去读取,那么内存中保留一份即可,但如果有地方需要写数据,那么会复制出一个新的地址空间存放相同数据,写操作作用在新地址空间上。
深拷贝和浅拷贝各有优劣,如果想兼有二者的优点,有两种可用方案:第一是使用引用计数,用shared_ptr的思路,每有一个指针指向对象,引用计数+1,直到引用计数清零时再清理内存;第二种是C++11的新标准移动语义move,把资源让渡,既可以避免重新创建空间,也防止空间释放导致新问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 class String { public : String (const char *str = NULL ); String (const String &other); String (String&& other); ~String (void ); String& operator = (const String& other); String& operator =(String&& rhs)noexcept ; friend ostream& operator <<(ostream& os, const String &c); private : char *m_data; }; String::String (const char *str) { if (str == NULL ) { m_data = new char [1 ]; if (m_data != NULL ) { *m_data = '\0' ; } else { exit (-1 ); } } else { int len = strlen (str); m_data = new char [len + 1 ]; if (m_data != NULL ) { strcpy (m_data, str); } else { exit (-1 ); } } } String::String (const String &other) { int len = strlen (other.m_data); m_data = new char [len + 1 ]; if (m_data != NULL ) { strcpy (m_data, other.m_data); } else { exit (-1 ); } } String::String (String&& other) { if (other.m_data != NULL ) { m_data = other.m_data; other.m_data = NULL ; } } String& String::operator = (const String &other) { if (this == &other) { return *this ; } delete [] m_data; int len = strlen (other.m_data); m_data = new char [len + 1 ]; if (m_data != NULL ) { strcpy (m_data, other.m_data); } else { exit (-1 ); } return *this ; } String& String::operator =(String&& rhs)noexcept { if (this != &rhs) { delete [] m_data; m_data = rhs.m_data; rhs.m_data = NULL ; } return *this ; } String::~String (void ) { if (m_data != NULL ) { delete [] m_data; } } ostream& operator <<(ostream& os, const String &c) { os << c.m_data; return os; } int main () { String s1 ("Hello" ) ; cout << s1 << endl; String s2 (s1) ; cout << s2 << endl; String s2A (std::move(s1)) ; cout << s2A << endl; String s3; cout << s3 << endl; s3 = s2; cout << s3 << endl; String s3A; s3A = std::move (s2A); cout << s3A << endl; return 0 ; }
析构函数
构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配资源后,就需要一个对应的操作来自动回收或释放资源。析构函数就是这样的特殊函数,作为构造函数的补充来完成所需的资源回收。当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。
不管类是否定义了自己的析构函数,编译器都将自动执行类中的非static数据成员的析构函数。
构造函数不能被定义为虚函数,但析构函数可以被定义为虚函数。一般来说,如果类中定义了虚函数,那么析构函数也应被定义为虚析构,尤其是类内有申请的动态内存需要被清理和释放。
假设类B是从类A继承的,如果类A的析构不是虚函数,那么如果基类指针pA指向new出来的B对象时,delete基类指针,只会运行基类的析构函数,而不会运行派生类的析构函数,造成资源释放不彻底。
无论程序员是否定义了自己的析构函数,都会创建和运行合成析构函数。如果程序员自己定义了析构,那么再执行完自定义析构后会运行合成析构函数。合成析构函数按照对象创建时的逆序撤销每个非static成员,即声明次序的逆序。对于类类型的数据成员,合成析构函数调用该数据成员的析构函数来撤销对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class A {public : A () { cout << "A" ; } ~A () { cout << "~A" ; } }; class B {public : B (A &a):_a(a) { cout << "B" ; } private : A _a; }; int main () { A a; B b (a) ; return 0 ; }
构造和析构的调用顺序
单继承
派生时,构造和析构不能被继承,为了对基类成员初始化,必须对派生类重新定义构造和析构,并在构造函数的初始化列表中调用基类的构造函数。派生类创建对象时,系统首先通过派生类的构造函数来调用基类的构造函数,完成基类成员的初始化,然后对派生类中新增的成员进行初始化。
必须将基类的构造函数放在派生类的初始化列表中以调用基类构造函数,派生类构造函数调用顺序为:
完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成;
调用基类的构造函数完成基类成员的初始化;
若派生类中含对象成员、const成员或引用成员,则必须在初始化列表中完成对其初始化;
派生类构造函数体执行;
当对象被删除时,派生类的析构函数被执行。析构函数不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序先是执行派生类的析构函数,再执行基类的析构函数,这和执行构造函数的孙顺序正好相反。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class A {public : A () { cout << "A" ; } ~A () { cout << "~A" ; } }; class B : public A{ public : B (A &a):_a(a) { cout << "B" ; } ~B () { cout << "~B" << endl; } private : A _a; }; int main () { A a; B b (a) ; return 0 ; }
多继承
多继承时,派生类的构造函数初始化列表需要调用需要调用各个基类的构造函数。此时构造函数初始化列表只能控制用于初始化基类的值,并不能控制基类构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class ZooAnimal {}; class Bear : public ZooAnimal{}; class Endangered {}; class Panda : public Bear, public Endangered{}
虚继承
首先调用虚基类的构造函数,虚基类如果有多个,则虚基类构造函数的调用顺序是此虚基类在当前派生表中出现的顺序而不是它们在成员初始化表中的顺序。
所有虚基类的构造函数调用完毕后,按照“多继承”中的规则调用其他构造函数。
同名成员函数
对类层次中的同名函数来说,有三种关系:重载(overload)、覆盖(override)和隐藏(hide、oversee)。理清三种关系有助于写出高质量代码。
成员函数的重载
重载的概念很简单,只有在同一类定义中的同名成员函数才存在重载关系,主要特点是函数的参数类型和数目有所不同,但不能出现函数参数的个数和类型均相同,仅仅依靠返回值类型的不同来区分。这和普通函数的重载是完全一致的。
重载和成员函数是否是虚函数无关。
1 2 3 4 5 6 7 class A { virtual int fun () ; void fun (int ) ; void fun (double , double ) ; static int fun (char ) ; };
成员函数的覆盖
覆盖是指:在派生类中覆盖基类中的同名函数,要求基类函数必须是虚函数,且:
与基类的虚函数具有相同的参数个数;
与基类的虚函数有相同的参数类型;
与基类的虚函数有相同的返回类型:如果返回的是指针(或引用),派生类的覆盖函数可以返回相同的指针(或引用),也可以返回基类返回的类型的子类型的指针(或引用);
1 2 3 4 5 6 7 8 9 10 11 class A {public : virtual void fun1 (int , int ) { } }; class B :public A{public : void fun1 (int , int ) { } };
重载和覆盖的区别:
覆盖是子类和父类之间的关系,是垂直的;重载是同一类中不同方法之间的关系,是水平的;
覆盖要求参数列表相同,返回类型相同;重载要求参数列表不同,返回类型不要求;
覆盖关系中,调用方法体是根据对象的类型来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的;
成员函数的隐藏
隐藏是指在某些特殊情况下,派生类中的函数屏蔽了基类中的同名函数,这些情况有:
两个函数参数相同,但基类函数不是虚函数;
两个函数参数不同,无论基类函数是否是虚函数,基类函数都会被屏蔽;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class A {public : void fun (int xp) { cout << xp << endl; } }; class B : public A{public : void fun (int xp) {} }; B b ; b.fun (); b.A::fun (2 );
1 2 3 4 5 6 7 8 9 10 11 12 13 class A {public : virtual void fun (int xp) { cout << xp << endl; } }; class B : public A{public : void fun (char * xp) {} };
继承
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。已存在的用来派生新类的类为基类,也称父类;新产生的类为派生类,也称子类。
一个派生类可以从一个基类派生(单继承),也可以从多个基类派生(多继承)。
1 2 3 class <派生类名 > :<继承方式1 > <基类名1 > ,<继承方式2 > <基类名2 > ,...{ <派生类新成员 > };
派生类对象由多个部分组成:派生类本身定义的(非static)成员加上由基类(非static)成员组成的子对象。如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最底层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。
基类成员访问属性
派生类对基类成员的访问形式有以下两种:
内部访问:由派生类中新增的成员函数对基类继承来的成员的访问;
对象访问:在派生类外部,通过派生类的对象从基类继承来的成员的访问;
共有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。一定要区分公有继承派生类的对象和派生类的成员函数对基类的访问是不同的:
父类的public成员成为子类的public成员,可以被该子类中的函数(内部访问)及其友元函数访问,也可以由该子类的对象(外部访问)访问;
父类的private成员仍旧是父类的private成员,子类成员包括友元函数和子类对象都不允许访问;
父类的protected成员成为子类的protected成员,可以被该子类中的函数(内部访问)及其友元函数访问,但不可以由该子类的对象访问 ;
私有继承的特点是基类的所有成员都作为派生类的私有成员,且不能被这个派生类的子类访问。
保护继承的特点是基类所有的公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然私有。
继承导致的二义性
类间的转换
C++基本类型的指针之间不含有隐式转换(void*除外),需要显式转换。但用户自定义类型有特殊的转换规则:
私有继承和保护继承时不允许隐式转换;
公有继承下,派生类的对象、对象指针、对象引用可以赋值给基类的对象、对象指针、对象引用,此时发生了隐式转换。但反过来不允许:基类的对象、对象指针、对象引用不能赋值给派生类的对象、对象指针、对象引用,因为派生类中包含了基类的所有信息,但基类缺乏派生类新增的信息;
虽然基类的对象指针、对象引用不允许隐式转换为派生类的对象指针、对象引用,但可以通过强制转换(显式)成为派生类的对象指针、对象引用。注意只限于基类对象的指针和引用,基类对象本身不允许;
一个指向基类的指针可以用来指向该基类的公有派生类的任何对象,这是C++程序运行时多态性的关键;
无论是通过创建成员对象还是通过继承,当我们把一个类的子对象嵌入一个新类中时,编译器会把每一个子对象置于新对象中。当引入多重继承时,由于对象往上转换期间出现多个类,因而对象存在多个this指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 class base1 { char c[16 ]; public : void printthis1 () { cout << "base1 this=" << this << endl; } }; class base2 { char c[16 ]; public : void printthis2 () { cout << "base2 this=" << this << endl; } }; class member1 { char c[16 ]; public : void printthism1 () { cout << "member1 this=" << this << endl; } }; class member2 { char c[16 ]; public : void printthism2 () { cout << "member2 this=" << this << endl; } }; class mi :public base1, public base2 { member1 m1; member2 m2; public : void printthis () { cout << "m1 this=" << this << endl; printthis1 (); printthis2 (); m1.printthism1 (); m2.printthism2 (); } }; int main () { mi MI; cout << "sizeof(mi)=" << sizeof (mi) << endl; MI.printthis (); cout << endl; base1* b1 = &MI; base2* b2 = &MI; cout << "base 1 pointer=" << b1 << endl; cout << "base 2 pointer=" << b2 << endl; cout << endl; mi* b3 = &MI; if (b1 == b3) { cout << "b1==b3" << endl; } if (b2 == b3) { cout << "b2==b3" << endl; } return 0 ; }
多基继承
当继承基类时,在派生类中就获得了基类所有数据成员的副本,该副本称为子对象。
一般来说,在派生类中队基类成员的访问应当具有唯一性,但在多基继承时,如果多个基类中存在同名成员的情况,造成编译器无从判断具体要访问哪个基类中的成员,则称为对基类成员访问的二义性问题。
若两个基类中具有同名的数据成员或成员函数,应使用成员名限定来消除二义性。更好的办法是在子类中也定义一个同名函数,根据需要调用成员名限定函数,从而实现对基类同名函数的隐藏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class A {public : void printf () { cout << "this is A" << endl; } }; class B {public : void printf () { cout << "this is B" << endl; } }; class C : public A, public B{public : void disp () { A::printf (); B::printf (); } }
菱形继承
菱形继承如上图。只要菱形继承一出现,由于新类中存在重叠的子对象base,此时既引入了二义性,同时重叠的子对象也增加了额外的存储空间。
和多基继承一样,用成员名限定指明使用哪一个即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class A {public : void printf () { cout << "this is x in A" << endl; } }; class B :public A {};class C :public A {};class D :public B, public C {};int main () { D d; A* pa = (A*)(B*)&d; d.B::printf (); d.C::printf (); return 0 ; }
事实上,使用关键字virtual将基类A声明为虚基类,可有效解决上述问题 。
转换构造函数
可以用单个实参来调用的构造函数定义从形参类型到该类类型的一个隐式转换。
1 2 3 4 5 6 7 8 class Integral {public : Integral (int = 0 ); private : int real; } Integral integ = 1 ;
转换构造函数需要满足以下条件之一:
Integral类的定义和实现中给出了仅包括只有一个int类型参数的构造函数;
Integral类的定义和实现中给出了包含一个int类型的参数,且其他参数都有缺省值的构造函数;
Integral类的定义和实现中虽然不包含int类型参数,但包含一个非int类型参数如float类型,此外没有其他参数或者其他参数都有缺省值,且int类型参数可以隐式转换为float类型参数;
这种隐式的类型转换某些情况下可能导致预期之外的行为,因此可以将转换构造函数声明为explicit来禁止隐式转换。
1 2 3 4 5 6 7 8 9 10 11 class Integral {public : explicit Integral (int = 0 ) ; private : int real; } Integral integ2 (1 ) ; Integral integ3 = Integral (1 );
类型转换函数
通过上述的转换构造函数可以将一个指定类型的数据转换为类的对象。但是不能反过来将一个类的对象转换成其他类型的数据。如可以将int型对象转换为上面例子中的Integral,但是不能将Integral转换为int。为此,C++提供了一个称为类型转换函数的函数来解决这个转换问题。类型转换函数的作用是将一个类的对象转换成另一个类型的数据:
1 2 3 4 5 6 7 8 9 10 11 12 class Integral {public : Integral (int = 0 ); operator int () { return real; } private : int real; } Integral integ = 1 ; int i = integ;
定义类型转换函数必须要注意几点:
转换函数必须是成员函数,不能是友元形式;
转换函数不能指定返回类型,但在函数体内必须用return语句以传值的方式返回一个目标类型的变量;
转换函数不能有参数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class B :public A {};A a; B b; a = b; class B { operator A () ; } A a; B b; a = b; class A { A (const B&); } A a; B b; a = b;
虚函数多态
多态性是面向对象语言的基本特征。多态性可以简单概况为“一个接口,多种方法”。函数重载就是一种简单的多态,一个函数名(调用接口)对应着几种不同的函数原型(方法)。
更通俗的说,多态性是指“同一个操作作用于不同的对象就会产生不同的响应”。多态性分为静态多态和动态多态,其中函数重载和运算符重载属于静态多态性,虚函数属于动态多态性。
静态联边与动态联编
程序调用函数时,具体使用哪个代码块是由编译器决定的。以函数重载为例,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,称为联编或绑定(binding)。
编译器可以在编译过程中完成这种联编,在编译过程中进行的联编叫静态联编(static
binding)或早期联编(early binding)。
在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供一套称为“动态联编”(dynamic
binding)的机制,也叫晚期联编(late
binding),C++通过虚函数来实现动态联编。
只要在类成员函数原型前加一个关键字virtual即可。如果一个基类的成员函数定义为虚函数,那么,它在所有的派生类中也保持为虚函数;即使派生类中省略了virtual,也仍然是虚函数。派生类可以根据需要对虚函数进行重定义,要求与基类虚函数有相同的参数个数、相同的参数类型、相同的返回类型(如果基类返回的是指针,派生类重定义的虚函数可以返回基类返回类型的子类型)。
虚函数的访问可以通过对象名、指针和引用来访问:
通过对象名来访问时,编译器采用的是静态联编。通过对象名访问虚函数时,调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数;
通过指针访问非虚函数时,调用哪个函数取决于指针本身的类型;而通过指针访问虚函数时,编译器根据指针所指向对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关;
通过引用访问虚函数与指针访问类似,不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。虽然使用上有一定限制,但一定程度上提高了代码安全性,特别是在函数参数传递场合中,可以将引用理解为一种“受限制的指针”;
构造函数不能为虚函数。 假设B继承于A,声明B对象时需要先执行A的构造函数,再执行B的构造函数。虚构函数的性质是如果函数为虚函数,那么将只执行子类的函数,不执行父类的函数。一旦将构造函数声明为虚函数,那么父类将无法构造。此外,虚函数是不同类型的对象产生不同的动作,构造函数执行时还没有对象产生,无法使用virtual来完成想要完成的动作。
此外,普通函数也不能声明为虚函数,只能被overload(重载),不能被override(覆盖),声明为虚函数毫无意义。
静态成员函数也不支持成为虚函数。静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,它不归属于具体的对象,所以没有动态绑定的必要性。
友元函数也不支持成为虚函数,因为C++就不支持友元函数的继承。
内联函数和赋值运算符可以被声明为虚函数,但是毫无意义。内联函数是为了在代码中直接展开,减少函数调用花费的代价,而虚函数是为了在继承后,对象能够准确地执行自己的动作,这是不可能统一的。即使虚函数被声明为内联函数,编译器也根本不会把这样的函数内联展开;operator
=重载函数要求形参与类本身类型相同,所以基类的赋值操作符形参类型为基类类型,子类类型的重载函数形参要求为子类类型,即使声明为虚函数,也不能作为子类的赋值操作符。
构造函数和虚构函数的函数体内可以调用虚函数,但是应尽量避免这种行为。构造派生类对象时,首先运行基类构造函数初始化对象的基类部分,此时派生类部分是未初始化的;同样的,撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。可见这两种情况下,对象都是不完整的。为了适应这种不完整性,编译器将对象的类型视为在构造期间或析构期间发生了变化。在基类构造函数中或析构函数中,将派生类对象当作基类类型对象对待,即在构造或者析构函数中,哪怕使用了"this->虚函数"的形式调用,编译器仍然解释为静态联编的“本类名::虚函数名”。
虚函数表指针(vptr)和虚基类表指针(bptr)
C++在布局以及存取时间上主要的额外负担是由Virtual引起的,包括:
virtual function机制:用以支持一个有效率的“执行器绑定”;
virtual base
class:用以实现多次出现在继承体系中的基类,有一个单一而被共享的实体。
虚函数表指针
C++中有两种数据成员:static和nonstatic,以及三种类成员函数:static、nonstatic和virtual。
1 2 3 4 5 6 7 8 9 10 11 class Point {public : Point (float xval); virtual ~Point (); float x () const ; static int PointCount () ; protected : virtual ostream& print (ostream& os) const ; float _x; static int _point_count; };
C++对象模型中,非static数据成员被配置于每一个对象之内,static数据成员则被存放在所有的对象之外,通常被放置在程序的全局(静态)存储区内,故不会影响个别的对象大小。
static和非static函数也被放在所有的对象之外,通过this指针来访问。
virtual函数以两个步骤支持:
每一个类产生出一堆指向virtual
functions的指针,放在表格之中,这个表格成为virtual table(vtbl);
每一个对象被添加了一个指针,指向相关的virtual
table。通常这个指针被称为vptr(虚函数表指针)。vptr的设定和重置都由每一个类的构造函数、析构函数和复制构造函数自动完成。每一个类所关联的type_info信息(用以支持runtime
type identification,RTTI)也经由virtual
table被指出来,通常是放在表格的第一个slot处。
由此可见,非static对象成员必须和对象绑定,其余的成员可以脱离对象存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class A {public : string a; void f1 () { printf ("Hello world" ); } void f2 () { a = "Hello World" ; printf ("%s" , a.c_str ()); } virtual void f3 () { printf ("Hello World" ); } }; int main () { A* aptr = NULL ; aptr->f1 (); aptr->f2 (); aptr->f3 (); return 0 ; }
含静态变量、虚函数的类空间
sizeof应用在类时的处理和struct中的处理是一样的,只需要注意特殊情况:
类或结构体中的static成员不对类或结构体的大小产生影响;
函数不占用内部空间;
如果含有virtual函数,需要额外计算vptr指针的大小(4);
空类的大小为1;
虚函数表的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Base { public : virtual void f () { cout << "Base::f" << endl; } virtual void g () { cout << "Base::g" << endl; } virtual void h () { cout << "Base::h" << endl; } }; int main () { typedef void (*Fun) (void ) ; Base b; Fun pFun = NULL ; cout << "虚函数表地址:" << (int *)(&b) << endl; cout << "虚函数表 - 第一个函数地址:" << (int *)*(int *)(&b) << endl; pFun = (Fun) * ((int *)*(int *)(&b)); pFun (); pFun = (Fun) * ((int *)*(int *)(&b) + 1 ); pFun (); pFun = (Fun) * ((int *)*(int *)(&b) + 2 ); pFun (); return 0 ; }
这个示例可以看到,我们通过强行把&b转成int*,取得虚函数表地址,然后,再次取值就可以得到第一个虚函数的地址了。
单基继承时,派生类仅有一个vptr。派生类中的虚函数覆盖了父类中的同名函数。多基继承时,有几个基类就有几个vptr,每个vptr中派生类替换掉父类的同名函数。
虚基类表指针(pbtr)
C++支持单一继承,也支持多重继承,继承关系也可以指定为虚拟(virtual,也就是共享的意思)
1 2 3 4 class iostream : public istream, public ostream {...};class istream : virtual public ios {...};class ostream : virtual public ios {...};
类似这种iostream的继承方式称为菱形继承。即istream和ostream虚拟继承ios,然后iostream普通继承istream、ostream。
在虚拟继承的情况下,基类不管在继承串链中被派生多少次,永远只会存在一个实体。
虚拟继承基类的子类中,子类会增加某种形式的指针 ,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量。此指针被称为pbtr。在同时存在vptr和bptr时,某些编译器会将其进行优化,合并为一个指针。
虚拟继承时的构造函数
普通的多层继承,构造函数的调用是嵌套的,如由C1类派生C23类,C2类派生C3类,则各个构造函数结构如下形式:
1 2 C2 (总参数表):C1 (参数表)C3 (总参数表):C2 (参数表)
但是对于虚基类派生来说,A类虚拟派生B类、C类,D类继承B类,C类,则构造函数写法如下:
1 2 3 4 B (总参数表):A (参数表)C (总参数表):A (参数表)D (总参数表):B (参数表), C (参数表), A (参数表)
根据虚基派生的性质,类D中只有一份虚基类A的拷贝,因此A类的构造函数在D类中只能被调用一次。所以,从A类直接虚拟派生(B和C)和间接派生(D)类的对象中,其构造函数初始化列表里都必须列出虚基类A构造函数的调用。这种机制保证了不管有多少层继承,虚基类构造函数必须且只能被调用一次。
若在初始化列表中没有显式调用虚基类的构造函数,则将调用虚基类的默认构造函数,若虚基类没有定义默认构造函数,则编译出错。
纯虚函数
纯虚函数是一种特殊的虚函数,一般格式如下:
1 2 3 class <类名>{ virtual <类型> <函数名>(<参数表>) = 0 ; };
在许多情况下,在基类中不能对虚函数给出有实际意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
凡是含有纯虚函数的类称为抽象类。这种类不能声明对象,只能作为基类为派生类服务。除非在派生类中完全实现基类中所有的纯虚函数,否则派生类也是抽象类,不能实例化对象。
只定义了protected型构造函数的类也是抽象类。对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中都不能创建该类的对象,但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类。
动态运行时类型识别(RTTI)
通过运行时类型识别(RTTI),程序能够使用基类的指针或者引用来检索这些指针或引用所指对象的实际类型。C++提供了两个操作符提供RTTI:
typeid操作符,返回指针或者引用所指对象的实际类型;
dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用;
typedef表达式形如typeid(e),这里e是任意表达式或者类型名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Base *bp; Derived *dp; if (typeid (*bp) == typeid (*dp)) { } if (typeid (*bp) == typeid (Derived)) { } if (typeid (bp) == typeid (Derived)) { }
抽象法则
面向对象的误区:对象是对现实世界中具体物体的反映,继承是对物体分类的反映?这个观念是错误的。举个例子,现实生活中我们往往把正方形看作是长和宽都相等的特殊的长方形,如果把这个思想引入到C++中,可能会这么设计继承关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class rectangle { virtual void SetLength (double a) { …… } }; class square : public rectangle { virtual void SetLength (double a) { …… } }
上面设计了两个类,一个长方形类,一个正方形类,其中正方形类继承了长方形类以及内部的方法SetLength。当我们调用长方形类SetLength的方法时,我们只是修改了长方形的长,长方形的宽不受影响;但是如果我们调用了正方形对象的SetLength方法,不仅长会受到影响,宽也会受到影响。这个从面向对象的继承体系来说就有很大的问题了。所以我们不要把现实世界中的关系代入到面向对象编程中。
具体类型的抽象
一个普通的int型变量,可以完成加、减、乘、除、比较、输出、自增等等一系列操作;如果现在有一个自定义的复数类型,我们自然也希望可以像使用int型变量一样使用它,同时它对我们是一个黑盒,一种抽象,我们不需要关心内部是如何实现的。
操作符重载函数的名字为operator后跟着所定义的操作符的符号。像任何其他函数一样,操作符重载函数有一个返回值和一个形参表。
形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式this形参)。比如赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数、第二个形参对应右操作数。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是类的成员函数,比如赋值就必须是类的成员,所以this绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。
并非所有操作符都可以重载,“::”、“.*”、“.”、“?:”不可以重载。可以记忆:带“点”的不允许重载。
对于赋值操作符来说,和复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。但一定要区分复制构造函数和赋值运算符的区别。复制构造函数只有在对象实例化时才会被调用,也就是说,在复制构造函数调用期间,这个对象处于一个未决状态(直到复制构造函数被成功调用),另外复制构造函数不返回任何值,void都没有。而赋值运算符则在一个现存的对象被赋予新值的时候被调用,并且它有返回值。所以并不是出现了“=”就是调用赋值构造函数,必须要看到底是否产生了新的对象实例。
一个新创建的类,编译器默认生成:构造函数、复制构造函数、析构函数、赋值运算符重载函数、取址运算符重载函数、const取值运算符重载函数。其中基类的构造函数(包括拷贝构造)、析构函数、赋值操作符重载函数都不能被派生类继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #pragma once #include <iostream> using namespace std;class Complex { public : Complex (); Complex (double r, double i); virtual ~Complex (); Complex (const Complex& x); Complex& operator =(const Complex &c); double GetReal ( ) const { return _real; } void SetReal (double d) { _real = d; } double GetImage () const { return _image; } void SetImage (double i) { _image = i; } Complex operator +(const Complex &c) const ; Complex& operator +=(const Complex &c); Complex operator -(const Complex &c) const ; Complex& operator -=(const Complex &c); Complex operator *(const Complex &c) const ; Complex& operator *=(const Complex &c); Complex operator /(const Complex &c) const ; Complex& operator /=(const Complex &c); Complex& operator =(const Complex &c); bool operator ==(const Complex &c) const ; bool operator !=(const Complex &c) const ; bool operator >(const Complex &c) const ; bool operator >=(const Complex &c) const ; bool operator <(const Complex &c) const ; bool operator <=(const Complex &c) const ; Complex& operator ++(); Complex operator ++(int ); Complex& operator --(); Complex operator --(int ); friend ostream& operator <<(ostream& os, const Complex &x); friend istream& operator >>(istream& is, Complex &x); private : double _real; double _image; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 #include "stdafx.h" #include "complex.h" Complex::Complex () { _real = 0.0 ; _image = 0.0 ; } Complex::Complex (double r, double i) { _real = r; _image = i; } Complex::Complex (const Complex& c) { _real = c._real; _image = c._image; } Complex& Complex::operator = (const Complex& c) { if (this != &c) { _real = c._real; _image = c._image; } return *this ; } Complex::~Complex () { _real = _image = 0.0 ; } Complex Complex::operator + (const Complex& c) const { return Complex (_real + c._real, _image + c._image); } Complex& Complex::operator += (const Complex& c) { _real += c._real; _image += c._image; return *this ; } Complex Complex::operator -(const Complex &c) const { return Complex (_real - c._real, _image - c._image); } Complex& Complex::operator -=(const Complex &c) { _real -= c._real; _image -= c._image; return *this ; } Complex Complex::operator *(const Complex &c) const { return Complex (_real*c._real - _image*c._image, _real*c._image + _image*c._real); } Complex& Complex::operator *=(const Complex &c) { Complex tmp (*this ); _real = tmp._real*c._real - _image*c._image; _image = tmp._real*c._image + tmp._image*c._real; return *this ; } Complex Complex::operator /(const Complex &c) const { double t = c._real*c._real + c._image*c._image; return Complex ((_real*c._real - _image*(-c._image)) / t, (_real*(-c._image) + _image*c._real) / t); } Complex& Complex::operator /=(const Complex &c) { Complex tmp (*this ); double t = c._real*c._real + c._image*c._image; _real = (tmp._real*c._real - tmp._image*(-c._image)) / t; _image = (tmp._real*(-c._image) + tmp._image*c._real) / t; return *this ; } bool Complex::operator ==(const Complex& c) const { return (_real == c._real) && (_image == c._image); } bool Complex::operator !=(const Complex& c) const { return !( (_real == c._real) && (_image == c._image) ); } bool Complex::operator >(const Complex &c) const { return (_real > c._real) && (_image > c._image); } bool Complex::operator >=(const Complex &c) const { return (_real >= c._real) && (_image >= c._image); } bool Complex::operator <(const Complex &c) const { return (_real < c._real) && (_image < c._image); } bool Complex::operator <=(const Complex &c) const { return (_real <= c._real) && (_image <= c._image); } Complex& Complex::operator ++ () { _real++; _image++; return *this ; } Complex Complex::operator ++ (int ) { return Complex (_real++, _image++); } Complex& Complex::operator --() { _real--; _image--; return *this ; } Complex Complex::operator --(int ) { return Complex (_real--, _image--); } ostream& operator <<(ostream& os, const Complex &x) { os << "real value is " << x._real << " image value is " << x._image; return os; } istream& operator >> (istream& is, Complex &x) { is >> x._real >> x._image; return is; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int main () { Complex a (1.0 , 2.0 ) ; cout << a.GetReal () << endl; cout << a.GetImage () << endl; a.SetImage (2.0 ); a.SetReal (3.0 ); cout << a.GetReal () << endl; cout << a.GetImage () << endl; Complex a (3.0 , 2.0 ) ; Complex c; c = a + b; Complex d; d = c++; cout << d << endl; cin >> d; cout << d << endl; return 0 ; }
类中有个this指针,指向当前对象本身。
类创建后会系统默认创建一个构造函数,我们可以自己实现构造函数。但我们如果重写了构造函数,那么原始的默认构造函数就不存在了,如果想使用需要重新声明实现。
等号运算符也一样,系统会默认帮我们重载。不过我们最好不要过于相信系统默认的重载,在复杂情况下运算的结果可能不是我们想要的。
程序中的临时对象一定要注意优化,避免产生临时对象,否则会触发拷贝构造。
new和delete是可以被重载的运算符。但实际上我们不能重定义new和delete表达式的行为,能够被重载的是全局函数operator
new和operator delete。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class X {public : X () { cout << "constructor" << endl; } ~X () { cout << "destructor" << endl; } static void * operator new (size_t size) { cout << "new" << endl; return ::operaotr new (size); } static void operator delete (void * pointee) { cout << "delete" << endl; ::operator delete (pointee) ; } }; int main () { X* px = new X (); delete px; return 0 ; }
"X* px = new X()"中的new为new
operator(new运算符),它将调用类X中的operator
new,为该类分配空间,然后调用当前实例的构造函数。
new operator的特点:
调用operator new分配足够的空间,并调用相关对象的构造函数
不可以被重载
operator new的特点:
只分配所要求的空间,不调用相关对象的构造函数;
可以被重载;
重载时,返回类型必须声明为void*;
重载时,第一个参数类型必须为表达式要求分配的空间的大小(字节),类型为size_t;
重载时,可以带其他参数;
“delete px”中的delete为delete
operator,它将调用该实例的析构函数,然后调用类X中的operator
delete,以释放该实例占用的空间。
operator new与operator
delete和C语言中的malloc和free对应,只负责分配及释放空间。但使用operator
new分配空间必须使用operator
delete来释放,而不能使用free,因为它们对内存使用的登记方式不同。反过来也一样。
利用对构造newdelete,可以禁止一个类产生栈对象和堆对象:
产生堆对象的唯一方法是使用new操作,通过禁止使用new就可以禁止产生堆对象。由于new操作执行时会调用operator
new,而operator new是可以重载的。所以使operator
new为private属性即可禁止new操作。为了对称,最好将operator
delete也重载为private。
创建栈上的对象时不使用new操作,不需要向堆申请内存,而是直接调整堆栈指针以“挪出”适当大小的指针,将对象压栈,产生栈对象的方式是调用构造函数,当函数返回时调用其析构函数释放这个对象,然后调整栈顶指针回收那块栈内存。所以将构造函数和析构函数设置为private后,系统不能调用构造函数和析构函数,自然不能在栈上产生对象了。
抽象类型的抽象
数学中有不同的图形,比如长方形、原型、三角形;多种图形计算周长、面积的方法不同,但都需要一个计算方法。我们可以抽象出一个图形类Shape,用Shape类进行公共层面的抽象操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include "stdafx.h" #include <iostream> using namespace std;class Shape { public : virtual double Area () const = 0 ; virtual void Show () = 0 ; void SetColor (int color) { _color = color; } void Display () { cout << Area () << endl; } private : int _color; }; class Square : public Shape{ public : Square (double len) :_len(len) { } void Show () { cout << "Square" << endl; } double Area () const { return _len*_len; } private : double _len; }; class Circle : public Shape{ public : Circle (double radius) :_radius(radius) {} void Show () { cout << "Circle" << endl; } double Area () const { return 3.1415926 *_radius*_radius; } private : double _radius; }; class Triangle : public Shape{ public : Triangle (double len, double height) :_len(len), _height(height){} void Show () { cout << "Triangle" << endl; } double Area () const { return 0.5 *_len*_height; } private : double _len; double _height; }; int main () { const int shapeNum = 3 ; Square s1 (2.0 ) ; s1.SetColor (1 ); Circle c1 (2.0 ) ; Triangle t1 (2.0 , 3.0 ) ; Shape* shapes[shapeNum]; shapes[0 ] = &s1; shapes[1 ] = &c1; shapes[2 ] = &t1; for (unsigned int index = 0 ; index < shapeNum; index++) { shapes[index]->Display (); } cout << endl; cout << sizeof (s1) << endl; return 0 ; }
面向对象三大特性
封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问;封装可以使得代码模块化;
继承性:让某种类型对象获得另一个类型对象的属性和方法,继承可以扩展已经存在的代码;
多态性:同一事物在保有原来特点的情况下表现出不同事物的能力,即不同对象会产生不同的行为;多态的目的是为了接口重用;
面向对象是软件工程发展到一定阶段为了管理代码和数据提出的一种方法,它没有解决以前解决不了的问题,不是万能的,只是为我们便捷的开发出能适应快速变化的软件提供了可能。面向对象不是对现实世界的映射,但它的封装性可以把问题简化;它的继承性可以减少代码重复,避免重新发明轮子;它的多态可以实现灵活的功能扩充,提升开发效率;