900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > C++学习笔记(三)——面向对象的程序设计

C++学习笔记(三)——面向对象的程序设计

时间:2022-07-13 03:16:23

相关推荐

C++学习笔记(三)——面向对象的程序设计

目录

一、类和对象基础

基本知识

1.概念

2.使用类的成员变量和成员函数:

3.类成员可访问的范围

4.构造函数

5.复制构造函数

6.类型转换构造函数

7.析构函数

8.委托构造函数、

9.前向引用声明

提高

1.this指针

2.静态成员变量

3.成员对象和封闭类

4.常量对象和常量成员函数

5.友元

运算符重载

1.基本概念

2.赋值运算符的重载

3.运算符重载的友元

4.可变长数组类的实现

5.流插入运算符和流提取运算符的重载

6.类型转换运算符的重载

7.自增自减运算符的重载

继承

1.继承和派生的基本概念

2.继承关系和复合关系

3.覆盖和保护成员

4.派生类的构造函数

5.公有继承的赋值兼容规则

6.直接基类和间接基类:

7.protected继承和private继承:

8.基类与派生类的指针强制转换:

多态

1. 虚函数和多态的基本概念

2. 多态实例:魔法门之英雄无敌

3. 多态实例:几何形体程序

4. 多态的实现原理

5. 虚析构函数

6.纯虚函数和抽象类

二、输入输出和模板

输入输出流相关的类

1.类的派生关系:

2.概述:

3.标准流对象与重定向:

用流操纵算子控制输出格式

1.简介:

2.整数流的基数

3.控制浮点数精度的流操纵算子:

4.设置域宽的流操纵算子

5.用户自定义的流操纵算子

文件读写

1.创建文件

函数模板

类模板

1.类模板概述

2.函数模板作为类模板成员

3.类模板和非类型参数

类模板与派生,类模板与友元,类模板与静态成员变量

1.类模板与派生

2.类模板与友元

3.类模板与静态成员变量

一、类和对象基础

基本知识

1.概念

(1)成员变量和成员函数统称为类的成员。

(2)类定义出来的变量称为类的实例,即所谓的“对象”(注:是类定义的变量,如clock c中的那个c,而不是类里面的成员变量)。

(3)与结构变量一样,对象占用的内存空间大小,等于所有成员变量大小之和,并不包括成员函数。

(4)可以只在类内部声明成员函数,在类的外部这样去定义(看,在函数内部可以不经调用直接使用成员变量),但是要记住,成员函数的形参表还要原封不动的摆上去:

int CRectangle::Area(){return w * h;}

2.使用类的成员变量和成员函数:

(1)对象名.成员名

(2)指针 ->成员名

CRectangle r1, r2;CRectangle *p1 = & r1;CRectangle *p2 = & r2;p1 -> w = 5;p2 -> Init(5, 4);

(3)引用名.成员名(这里顺便教一下怎么把对象作为函数参数):

CRectangle r2;CRectangle &rr = r2;rr.w = 5;rr.Init(5, 4);

Void PrintRectangle(CRectangle &r){cout << r.Area();}CRectangle r3;r3.Init(5, 4);PrintRectangle(r3);

3.类成员可访问的范围

(1)private(私有成员):成员函数内;public(共有成员):任何地方;protected(保护成员):略。

(2)三种关键字的出现次数与出现顺序没有限制。

(3)缺省关键词时,默认的是私有成员。

(4)类的成员函数内部,能访问:当前对象的全部属性、函数;同类其它对象的全部属性,函数。

(5)类的成员函数以外,只能访问类的共有成员。

(6)成员函数也和普通函数一样,可以重载和参数缺省(即函数带默认值),规则和普通函数也是一样的。

4.构造函数

(1)函数名与类名一样,可以有参数,但是不能有返回值(void也不行)。

(2)作用是对对象进行初始化,如给成员变量赋初值。

(3)若没定义构造函数,则编译器自己生成默认的无参数的构造函数。默认构造函数无参数,也不做任何操作。若定义了构造函数,编译器就不在自动生成。因此,敲黑板!无参构造函数也称默认构造函数,不一定存在,当且仅当自己定义了构造函数。

(4)对象生成时构造函数自动被调用。对象一旦生成,再也不能在上面执行构造函数。

(5)一个类可以有多个构造函数,其规则满足函数重载规则,即参数个数或参数种类不同。

(6)构造函数的作用:可以代替初始化函数。

(7)仔细阅读以下构造函数用法的汇总:

#include<cstdio>class Complex{private:int real, imag;public:Complex(int i, int j = 0);//重载构造函数Complex(Complex c1, Complex c2){real = c1.real + c2.real;imag = c1.imag + c2.imag;}//没参数的构造函数Complex(){}};Complex::Complex(int i, int j) //看,这里声明时参数有默认值,这里可以不用跟下来,也不能跟下来,否则会报错。{real = i, imag = j;}int main(){//构造函数普通用法:Complex c1(2, 3);//构造函数用动态分配内存的用法:Complex* pc = new Complex(3, 4);//使用其他构造函数的用法:Complex c2(c1, *pc);//生成对象数组:Complex a[2] = {1, Complex(2, 3)};//用动态分配内存,生成对象数组,看清是怎么用的!Complex *pa[3] = {new Complex(), new Complex(1), new Complex(2, 3)};return 0;}

5.复制构造函数

(1)只有一个参数,即同类对象的引用。

(2)两种形式:X :: X( X& )或 X :: X( const X & ),通常选择后者,因为复制构造函数中一般不用修改参数的值。但是,不允许形如X::X(X)的复制构造函数,因为复制构造函数的参数必须是引用,不能是对象。

(3)如果没有定义复制构造函数,则编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。定义了复制构造函数就不再自动生成。

(4)复制构造函数起作用的三种情况:

当用一个对象去初始化同类另一个对象时;

#include<cstdio>class Complex{public:double real, imag;Complex(){}Complex(const Complex& c){real = c.real;imag = c.imag;}};int main(){Complex c1;//第一种情况两种用法:Complex c2(c1);Complex c3 = c1; //注意,这里是初始化语句,并非赋值语句。赋值语句样例往下看。}

如果某函数有一个参数是类A的对象,那么该函数被调用的时候,类A的复制构造函数将被调用;如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数将被调用。

#include<cstdio>class A{public:int v;A(int n){v = n;}A(const A& a){v = a.v;}};//第二种情况:void func2(A a1){}//第三种情况:A func3(){A a(4);return a;}int main(){A a2(3);//第二种情况,在这里调用复制构造函数;func2(a2);//第三种情况,在这里调用复制构造函数;printf("%d\n", func3().v);return 0;}

(5)注意,对象间的赋值并不导致复制构造函数被调用。

#include<cstdio>class C{public:int n;C(){}C(C& c){ n = 2 * c.n; }};int main(){C c1, c2;//对象间赋值不调用复制构造函数,因此c2.n仍然是5;c1.n = 5; c2 = c1;//一下两句话做对比,刚才也提到,这是复制构造函数被调用的第一种情况。此时c3.n和c4.n都是10。C c3(c1);C c4 = c1;return 0;}

(6)类似这样的函数,调用生成形参是会发生复制构造函数被调用,开销比较大。

void fun(C c){cout << "fun" << endl;}

因此可以用C&引用类型作为参数,以防在函数中实参被改变,可以加上const关键字。

void fun(const C& c){cout << "fun" << endl;}

6.类型转换构造函数

(1)定义类型转换构造函数的目的是实现类型的自动转换。

(2)只有一个参数,而且不是复制构造函数的构造函数,一般可以看做是转换构造函数。

(3)当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)

#include<iostream>using namespace std;class Complex{public:double real, imag;Complex(int i) {real = i; imag = 0;}Complex(double r, double i) {real = r; imag = i;}};int main(){Complex c1(7, 8);Complex c2 = 12;c1 = 9;//这里,9先是转换成一个临时Complex对象,然后这个临时对象复制给c1。//还记得构造函数那节课讲过吗?这个是赋值,是不会调用构造函数的。return 0;}

7.析构函数

(1)名字和类名相同,在前面加‘~’,没有参数和返回值,一个类最多只能有一个析构函数。

(2)析构函数在对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放内存空间。

(3)如果定义类的时候没有写析构函数,则编译器自动生成缺省析构函数,缺省析构函数什么也不做。

(4)如果自定义了析构函数,那么编译器不生成缺省析构函数。

对象数组生命周期结束时,对象数组的每个元素的析构函数都会被调用。

#include<iostream>using namespace std;class String{private:char *p;public:String() {p = new char[10];}~String();};String :: ~String(){delete []p;}

delete运算会导致析构函数被调用。但是,需要强调的是,new申请的内存,若不delete掉,在程序运行结束的时候也不会自行释放的。还有,若new一个对象数组,那么用delete释放时应该写[]。否则只delete一个对象(即只调用一次析构函数)。

#include<iostream>using namespace std;class C{public:int x;~C(){cout << "destructor" << endl;}};int main(){C* p = new C;delete p; //析构函数在这里调用一次。p = new C[3];delete []p; //析构函数在这里被调用三次。return 0;}

析构函数在对象作为返回值的返回的时候被调用。

#include<iostream>using namespace std;class C{public:int x;~C(){cout << "destructor" << endl;}};C obj;C func(C c) //参数消亡调用一次析构函数{//函数调用返回时生成临时对象返回。return c;}int main(){obj = func(obj);//函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用。return 0;}

8.委托构造函数

(1)委托构造函数使用类的其他构造函数执行初始化过程。

class Clock {private:int Hour, Minute, Second;public:Clock(int newH, int newM, int newS) {}Clock() :Clock(0, 0, 0) {}};

9.前向引用声明

class B;class A {public:void f(B b);};class B {public:void g(A a);};

(1)类应该先声明,再引用。

(2)如果需要在某个类的声明之前,引用该类,则应进行前向引用声明。

(3)前向引用声明只为程序引入一个标识符,但具体声明在其他地方。

(4)在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。

(5)当使用前向引用声明的时候,只能使用被声明的符号,而不能涉及类的任何细节。

提高

1.this指针

(1)作用:指向成员函数所作用的对象

(2)非静态成员函数可以直接使用this来代表指向该函数作用的对象的指针。

#include<iostream>using namespace std;class Complex{public:double real, imag;void Print(){cout << real << "," << imag << endl;}Complex(double r, double i):real(r), imag(i){}Complex AddOne(){this -> real++; //等效于real++;this -> Print(); //等效于Print();return *this;}};int main(){Complex c1(1, 1), c2(0, 0);c2 = c1.AddOne();return 0;}

(3)非静态成员函数真实的参数的个数,比程序写出的参数个数多1个,多的就是那个this指针。

(4)静态成员函数中不能使用this指针,因为静态成员函数并不具体作用于某个对象。因此静态成员函数的真实的参数的个数,就是程序中写出的参数的个数。

(5)说句题外话,我们来看一个程序。虽说我们强调空指针不是这样用的,但是这样确实不会报错。

#include<iostream>using namespace std;class A{public:int i;void Hello() {cout << "hello" << endl;}/*等效于:void Hello(A* this) {cout << "hello" << endl;}*/};int main(){A* p = NULL;p -> Hello(); //等效于:Hello(p),因此你不会报错。return 0;}

对比一下,这个程序就会崩溃:

#include<iostream>using namespace std;class A{public:int i;void Hello() {cout << i << "hello" << endl;}/*等效于:void Hello(A* this) {cout << this -> i << "hello" << endl;}*/};int main(){A* p = NULL;p -> Hello(); //等效于:Hello(p);return 0;}

2.静态成员变量

(1)普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。因此,sizeof 运算符不会计算静态成员变量。

(2)普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。因此静态成员不需要通过对象就能访问。

(3)静态成员变量本质上也是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。同理,静态成员函数本质上也是全局函数。

(4)设置静态函数这种机制的目的是,将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。

(5)必须在定义类的文件里面对静态成员变量进行一次说明或初始化,否则编译能通过,链接不能通过。

(6)在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

(4)访问静态成员变量的四种方法:

类名::成员名对象名.成员名指针 ->成员名引用.成员名

#include<iostream>using namespace std;class C {public:static int n;static void Print() {cout << "P\n";}};int C::n = 0;// 必须在定义类的文件中对静态成员变量进行一次说明或初始化。否则编译能通过,链接不能通过。int main() {//类名::成员名C::Print();//对象名.成员名C r;r.Print();//指针 ->成员名C* p = &r;p->Print();//引用.成员名C& ref = r;int x = ref.n;cout << x << endl;return 0;}

3.成员对象和封闭类

(1)有成员对象的类叫封闭类。任何生成封闭类的语句,都得让编译器明白,对象中的成员对象,是如何初始化的。具体做法就是通过封闭类的构造函数的初始化列表。成员对像的初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行。

(2)封闭类构造函数和析构函数的执行顺序

封闭类对象生成时,先执行所有成员对像的构造函数,然后才执行封闭类的构造函数。对象成员的构造函数调用次序与对象在类中的说明次序一致,和它们在成员初始化列表中出现的次序无关。当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,次序和构造函数的调用次序相反。

(3)举个栗子(主要看一下初始化列表是怎么使用的):

#include<iostream>using namespace std;class Tyre{private:int radius;int width;public:Tyre(int r, int w):radius(r), width(w){}};class Engine{};class Car{private:int price;Tyre tyre;Engine engine;public:Car(int p, int tr, int tw);};Car::Car(int p, int tr, int w):price(p), tyre(tr, w){} //形参也可以字母不一样啊。int main(){Car car(20000, 17, 225);return 0;}

4.常量对象和常量成员函数

(1)如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加const关键字。

(2)在类的成员函数后面可以加const关键字,则该成员函数成为常量成员函数。这里,const是函数类型的一个组成部分,因此在实现部分也要带const关键字。

(3)常量成员函数执行期间不应修改其作用的对象。因此,在常量成员函数中不能修改成员变量的值(静态变量除外),也不能调用同类的非常量成员函数(静态变量除外)。

(4)两个成员函数,名字和参数表都一样,但一个是const,一个不是,算重载。

(5)引用前面可以加const关键字,成为常引用,不能通过常引用,修改其引用的变量。

(6)通过常对象只能调用它的常成员函数。

5.友元

(1)友元分为友元函数和友元类两种。

(2)友元函数:一个类的友元函数可以访问这个类的私有成员,可以将一个类的成员函数(包括构造,析构函数)说明为另一个类的友元。

(3)亲测得知,那个ModifyCar函数里面的那个pCar可以把指针换成引用,不会影响什么。而程序第三行那个class Car就是前向引用声明,就是郑莉老师讲的那个稀奇古怪的东西。

#include<iostream>using namespace std;class Car;class Driver {public:void ModifyCar(Car* pCar);};class Car {private:int price;friend int MostExpensiveCar(Car cars[], int total) {int tmpMax = -1;for (int i = 0; i < total; i++) {if (cars[i].price > tmpMax) tmpMax = cars[i].price;}return tmpMax;}friend void Driver::ModifyCar(Car* pCar);public:Car(int x) :price(x) {};void Print() {cout << price << endl;}};void Driver::ModifyCar(Car* pCar) {pCar->price += 100;}int main() {Car* p = new Car(10);p->Print();Driver d;d.ModifyCar(p);p->Print();return 0;}

(3)友元类:如果A是B的友元类,那么A的成员函数可以访问B的私有成员。友元类之间的关系不能传递,也不能继承。

(4)类的友元关系是单向的。类的友元关系是单向的,声明B类是A类的友元,A类此时没有同时成为B类的友元。

#include<iostream>using namespace std;class Car{private:int price;friend class Driver;};class Driver{public:Car myCar;void ModifyCar(){myCar.price += 1000;}};int main(){return 0;}

运算符重载

1.基本概念

(1)一点点解释:

运算符重载的实质是函数重载。可以重载为普通函数,也可以重载为成员函数。把含运算符的表达式转换成对运算符函数的调用。把运算符的操作数转换成运算符函数的参数。运算符被多次重载时,根据实参的类型决定调用那个运算符函数。

(2)重载为成员函数时,参数个数为运算符目数减1;重载为普通函数的时候,参数个数为运算符目数。

(3)举个栗子:

#include<iostream>#include<cstring>using namespace std;class Complex{public:double real, imag;Complex(double r = 0.0, double i = 0.0):real(r), imag(i){}Complex operator - (const Complex & c);};Complex operator + (const Complex & a, const Complex & b){return Complex(a.real + b.real, a.imag + b.imag);}Complex Complex::operator - (const Complex & c){return Complex(real - c.real, imag - c.imag);}

(4)运算符重载注意事项(这些内容是郭炜老师的在第7部分讲的,我把它放在前面):

C++不允许定义新的运算符重载后的运算符的含义应该符合日常习惯运算符重载不改变运算符的优先级;一下运算符不可以被重载:“.”、“.*”、“::”、“?:”、sizeof;重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数。

2.赋值运算符的重载

(1)赋值运算符可以重载,使两边的类型不匹配。赋值运算符“=”只能重载为成员函数。

#include<iostream>#include<cstring>using namespace std;class String{private:char * str;public:String():str(new char[1]) {str[0] = 0;}const char * c_str() {return str;}// return的str是指针,函数定义返回值也得是个指着啊String & operator = (const char * s);~String() {delete []str;}};String & String::operator = (const char * s){delete []str;str = new char[strlen(s) + 1]; //+1的目的是为了放'\0'strcpy(str, s);return *this;}int main(){String s;s = "Good Luck!";//调用的c_str()其实是个指针函数,返回的也是个指针,不过,输出字符串不是只输出指针就可以了吗?cout << s.c_str() << endl;/*String s2 = "Hello"上面那句话是错误的,因为此时的等号表示这是初始化语句而不是赋值语句,而是调用构造函数。而前文定义的构造函数只是初始化字符而非字符串。*/return 0;}

(2)我们看这个程序,假如上面那个类不做修改的话会出以下问题:

int main(){String s1, s2;s1 = "this", s2 = "that";s1 = s2;return 0;}

如不第一自己的赋值运算符,那么s1 = s2实际上导致s1.str和 s2.str指向同一个地方。如果s1的对象消亡,析构函数将释放s1.str指向的空间,而s2消亡时还要再释放一次,不妥。另外,如果执行s1 = "other";会导致s2.str指向的地方被delete,而我们的本意是只想改变s1而不改变s2。

(3)因此,我们加入这样一个成员函数:

String & operator = (const String & s){//若参数s引用的自己,那么要先判断一下,不然会导致自己的内存空间被释放。if(this == & s) return * this;delete [] str;str = new char[strlen(s.str) + 1];strcpy(str, s.str);return * this;}

(4)operator =的返回值是 String &的目的是:对运算符重载是,好的风格应该是尽量保留运算符原本的特性。

考虑到原本的赋值运算符还有如下特性:a = b = c,(a = b) = c。

(5)还有一个问题,如果不为String类编写复制构造函数,会和(2)出现一样的那个问题,因此要自己编写复制构造函数。当然,这个对象刚刚生成,不会等于自己本身,因此无需考虑是不是和自己相等。

String(String & s){str = new char[strlen(s.str) + 1];strcpy(str, s.str);}

3.运算符重载的友元

有些时候,要解决访问私有成员的问题,要让 c + 5和 5 +c 都成立,因此需要把运算符重载为友元函数。

#include<iostream>using namespace std;class Complex{double real, imag;public:Complex(double r, double i):real(r), imag(i){};Complex operator + (double r);friend Complex operator + (double r, const Complex & c);};Complex Complex::operator + (double r){return Complex(r + real, imag);}Complex operator + (double r, const Complex & c){return Complex(c.real + r, c.imag);}

4.可变长数组类的实现

5.流插入运算符和流提取运算符的重载

(1)cout是在iostream中定义的,ostream类的对象。"<<: 能用在 cout上面是因为,在 iostream 里对 "<<"进行了重载。但是怎么让 cout << 5 << "This" 这一长串的东西成立?那么可以让"<<"返回值为仍为ostream的类,然后接着调用此函数。

(2)这样 cout << 5 << "This",本质上就是cout.operator << (5).operator << ("This")。

(3)当然,直接输出一个对象不一定非要重载 "<<"运算符,还可以重载类型转换运算符。

ostream & ostream::operator << (int n){//...输出n的代码return *this;}ostream & ostream::operator << (const char* s){//...输出s的代码return *this;}

(4)流插入运算符的重载

#include<iostream>using namespace std;class Student{public:int age;};ostream & operator << (ostream & o, const Student &s){o << s.age;return o;}int main(){Student s;s.age = 5;//输出Student中的age,需要重载<<运算符。cout << s << "Hello";return 0;}

(5)来一道例题看看。

/*假定c是Complex复数类的对象,现在希望写“cout << c;”,就能以“a+bi”的形式输出c的值,写“cin>>c;”,就能从键盘接受“a+bi”形式的输入,并且使得c.real = a,c.imag = b。*/#include<iostream>#include<string>#include<cstdlib>using namespace std;class Complex{double real, imag;public:Complex(double r = 0, double i = 0):real(r), imag(i){};friend ostream & operator << (ostream & os, const Complex &c);friend istream & operator >> (istream & is, Complex & c);};ostream & operator << (ostream & os, const Complex & c){os << c.real << '+' << c.imag << "i";return os;}istream & operator >> (istream & is, Complex & c){string s;is >> s;int pos = s.find("+", 0);string sTmp = s.substr(0, pos);c.real = atof(sTmp.c_str());sTmp = s.substr(pos + 1, s.length() - pos - 2);c.imag = atof(sTmp.c_str());return is;}int main(){Complex c;int n;cin >> c >> n;cout << c << "," << n;return 0;}

6.类型转换运算符的重载

(1)很简单,需要记住重载类型转换符的时候前面不用声明返回值的类型,因为重载什么类型转换运算符就是返回什么类型啊。因为C++有一条规矩,重载后的运算符的含义应该符合日常习惯。看一个简单的栗子。

#include<iostream>using namespace std;class Complex{double real, imag;public :Complex(double r = 0, double i = 0):real(r), imag(i){};//重载强制类型转换运算符double,且operator前不写返回值类型。operator double() {return real;}};int main(){Complex c(1.2, 3.4);cout << (double)c << endl;double n = 2 + c;cout << n;return 0;}

7.自增自减运算符的重载

(1)自增运算符++、自减运算符-- 都有前置/后置之分,为了区分所重载的运算符是前置运算符还是后置运算符, C++规定:前置运算符作为一元运算符重载;后置运算符作为二元运算符重载,多写一个没有用的参数。

(2)但是,在没有后置运算符重载而有前置运算符重载的情况下:在VS中,obj++也调用了前置重载,而dev则令 obj++编译出错。

(3)实际上,在C++中, ++a返回值就是a的引用,因此重载前置运算符时要保持原有特性。同理,a--返回值就是临时变量,而不是引用,因此后置运算符返回值就是一个临时对象。因此,前置运算符是更快的,因为没有生成临时对象。

(4)想用 cout 直接输出一个对象的成员变量,可以重载"<<",或者重载类型转换运算符。

#include<cstdio>//前置运算符作为一元运算符重载//重载为成员函数:T & operator++();T & operator--();//重载为全局函数T1 & operator++(T2);T1 & operator--(T2);//后置运算符作为二元运算符重载//重载为成员函数:T operator++(int);T operator--(int);//重载为全局函数T1 operator++(T2, int);T1 operator--(T2, int);

(5)看一个例子:

#include<iostream>using namespace std;class Demo{private:int n;public:Demo(int i = 0):n(i){}Demo & operator++();Demo operator++(int);operator int() {return n;}friend Demo & operator--(Demo &);friend Demo operator--(Demo & , int);};Demo & Demo::operator++(){++n;return *this;}Demo Demo::operator++(int k){Demo tmp(*this); //记录修改前的对象n++;return tmp; //返回修改前的对象}// s++即为: s.operator++(0);Demo & operator--(Demo & d){d.n--;return d;}Demo operator--(Demo & d, int){Demo tmp(d);d.n--;return tmp;}

继承

1.继承和派生的基本概念

(1)继承:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类,而把B作为基类的一个派生类(也称子类) 。

(2)派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。派生类一经定义后,可以独立使用,不依赖于基类。

(3)派生类拥有基类的全部成员函数和成员变量,不论是private、protected、public 。但是,在派生类的各个成员函数中,不能访问基类中的private成员。

(4)继承派生模型:

class 派生类名:public 基类名 {};

(5)派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。

(6)看一个例子:

#include<iostream>#include<string>using namespace std;class CStudent {private:string name;public:void PrintInfo();void SetInfo(const string& name_);string Getname() { return name; }};class CUndergraduateStudent : public CStudent {private:string department;public:void PrintInfo() {CStudent::PrintInfo();cout << "Department: " << department << endl;}void SetInfo(const string& name_, const string& department_) {CStudent::SetInfo(name_);department = department_;}void QualifiedForBaoyan() {cout << "qualified for baoyan" << endl;}};void CStudent::SetInfo(const string& s) {name = s;}void CStudent::PrintInfo() {cout << "Name: " << name << endl;}int main() {CUndergraduateStudent s2;s2.SetInfo("Harry Potter", "Computer Science");cout << s2.Getname() << " ";s2.QualifiedForBaoyan();s2.PrintInfo();return 0;}

2.继承关系和复合关系

(1)继承关系:

本质:“是”关系。类B是基类A的派生类逻辑上要求:“一个B对象也是一个A对象”。

(2)复合关系:

本质:“有”关系。若类C中“有”成员变量k,k是类D的对象,则C和D是复合关系一般逻辑上要求:“D对象是C对象的固有属性或组成部分”。

(3)例子:

如果要写一个小区养狗管理程序,需要写一个“业主”类,还需要写一个“狗”类。假定狗只有一个主人,但一个业主可以有最多10条狗。为“狗”类设一个“业主”类的对象指针;为“业主”类设一个“狗”类的对象指针数组。

class CMaster; //CMaster必须提前声明,不能先写CMaster类后写Cdog类class CDog {CMaster * pm;};class CMaster {CDog * dogs[10];};

3.覆盖和保护成员

(1)覆盖:派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::。

(2)看一个例子:

#include<iostream>using namespace std;class base {int j;public:int i;void func(){}};class derived :public base {public:int i;void access();void func(){}};void derived::access() {i = 5; //引用的是派生类的ibase::i = 5; //引用的是基类的ifunc(); //派生类的成员函数base::func(); //基类的成员函数}int main() {//在类外,规则是类似的。derived obj;obj.i = 1; //派生类的iobj.base::i = 1; //基类的i return 0;}

(3)一般来说,基类和派生类不要定义同名成员变量,按照上面那个例子可以这样访问,但是容易出错。

(4)基类的private成员:可以被下列函数访问

基类的成员函数基类的友员函数

(5)基类的protected成员:可以被下列函数访问

基类的成员函数基类的友员函数派生类的成员函数可以访问当前对象的基类的保护成员

(6)基类的public成员:可以被下列函数访问

基类的成员函数基类的友员函数派生类的成员函数派生类的友员函数其他的函数

4.派生类的构造函数

(1)在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

(2)调用基类构造函数的两种方式:

显式方式:在派生类的构造函数中,为基类的构造函数提供参数:

derived::derived(arg_derived_list):base(arg_base_list)隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数.

(3)派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。

(4)看一个派生类构造函数例子:

#include<iostream>using namespace std;class Bug {private:int nLegs, nColor;public:int nType;Bug(int legs, int color) :nLegs(legs), nColor(color) {};void PrintBug() {};};class FlyBug :public Bug {int nWings;public:FlyBug(int legs, int color, int wings) :Bug(legs, color), nWings(wings) {};};

(5)包含成员对象的派生类的构造函数写法:

#include<iostream>using namespace std;class Bug {private:int nLegs, nColor;public:int nType;Bug(int legs, int color) :nLegs(legs), nColor(color) {};};class Skill {public:Skill(int n) {};};class FlyBug :public Bug {int nWings;Skill sk1, sk2;public:FlyBug(int legs, int color, int wings) :Bug(legs, color), sk1(5), sk2(color), nWings(wings) {};};

(6)封闭派生类对象的构造函数执行顺序:

先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。最后执行派生类自己的构造函数

(7)封闭派生类对象消亡时析构函数的执行顺序

先执行派生类自己的析构函数再依次执行各成员对象类的析构函数最后执行基类的析构函数析构函数的调用顺序与构造函数的调用顺序相反。

5.公有继承的赋值兼容规则

(1)public继承的赋值兼容规则

class base { };class derived : public base { };base b;derived d;

派生类的对象可以赋值给基类对象:b = d;派生类对象可以初始化基类引用:base & br = d;派生类对象的地址可以赋值给基类指针:base * pb = & d;如果派生方式是 private或protected,则上述三条不可行。

6.直接基类和间接基类:

(1)概念:类A派生类B,类B派生类C:则类A是类B的直接基类;类B是类C的直接基类,类A是类C的间接基类。

(2)在声明派生类时,只需要列出它的直接基类;派生类沿着类的层次自动向上继承它的间接基类。

(3)派生类的成员包括:

派生类自己定义的成员直接基类中的所有成员所有间接基类的全部成员

(4)派生类的构造函数中,只需要指明其直接基类是如何初始化的即可,不需要说明间接基类如何初始化。构造函数执行顺序是基类 ->派生类 ->派生类,而析构函数执行顺序正好相反。

#include<iostream>using namespace std;class Base {public:int n;Base(int i) :n(i) {cout << "Base constructed\n";}~Base(){cout << "Base destructed\n";}};class Derived :public Base {public:Derived(int i) :Base(i) {cout << "Derived constructed\n";}~Derived(){cout << "Derived destructed\n";}};class MoreDerived :public Derived {public:MoreDerived() :Derived(6) {cout << "MoreDerived constructed\n";}~MoreDerived() {cout << "MoreDerived destructed\n";}};int main() {MoreDerived Obj;return 0;}/*Output:Base constructedDerived constructedMoreDerived constructedMoreDerived destructedDerived destructedBase destructed*/

7.protected继承和private继承:

(1)以protected继承为例

class base {};class derived : protected base {};base b;derived d;

(2)规定:

protected继承时,基类的public成员和protected成员成为派生类的protected成员。private继承时,基类的public成员和protected成员,都成为派生类的private成员派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类中的private 成员。通过派生类的对象:不能直接访问从基类继承的任何成员。protected和private继承不是“是”的关系。

8.基类与派生类的指针强制转换:

(1)公有派生的情况下,派生类对象的指针可以直接赋值给基类指针:

Base * ptrBase = &objDerived;ptrBase指向的是一个Derived类的对象;*ptrBase可以看作一个Base类的对象,访问它的public成员直接通过ptrBase即可,但不能通过ptrBase访问objDerived对象中属于Derived类而不属于Base类的成员

(2)即便基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类没有而派生类中有的成员。

通过强制指针类型转换,可以把ptrBase转换成Derived类的指针Base * ptrBase = &objDerived;Derived *ptrDerived = (Derived * ) ptrBase;

(3)程序员要保证ptrBase指向的是一个Derived类的对象,否则很容易会出错。

多态

1. 虚函数和多态的基本概念

(1)虚函数:在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。virtual 关键字只用在类定义里的函数声明中,

写函数体时不用。

(2)多态的表现形式一:派生类的指针可以赋给基类指针。通过基类指针调用基类和派生类中的同名 虚函数时:

若该指针指向一个基类的对象,那么被调用是基类的虚函数;若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

(3)多态的表现形式二:派生类的对象可以赋给基类引用。通过基类引用调用基类和派生类中的同名虚函数时:

若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。

#include<iostream>using namespace std;class CBase {public:virtual void SomeVirtualFunction();};class CDerived :public CBase {public:virtual void SomeVirtualFunction();};void CBase::SomeVirtualFunction() {cout << "CBase\n";}void CDerived::SomeVirtualFunction() {cout << "CDerived\n";}int main() {CDerived ODerived;CBase* p = &ODerived;p->SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象cout << "#\n";CBase& r = ODerived;r.SomeVirtualFunction(); ///调用哪个虚函数取决于r引用哪种类型的对象return 0;}/*Output:CDerived#CDerived*/

(4)多态的作用:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

2. 多态实例:魔法门之英雄无敌

3. 多态实例:几何形体程序

4. 多态的实现原理

(1)“多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 ---- 这叫“动态联编”。

(2)多态实现的关键 --- 虚函数表:每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的4个字节就是用来放虚函数表的地址的。

(3)多态很好用,但也会多出时间和空间上的开销。空间开销就是每个对象都会多出四个字节,时间开销就是要通过一系列指令(比如查虚函数表),才可以调用函数。但是多态会大大节省人力资源,因为它使得程序维护起来很方便。

5. 虚析构函数

(1)虚析构函数:通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,我们认为,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。举个栗子:

#include<iostream>using namespace std;class son {public:~son() {cout << "bye from son" << endl;}};class grandson :public son {public:~grandson() {cout << "bye from grandson" << endl;}};int main() {son* pson = new grandson();delete pson;return 0;}/*Output:bye from son*/

(2)解决办法:把基类的析构函数声明为virtual。

基类的析构函数声明为virtual,派生类的析构函数可以不加virtual,它也是虚函数。通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数。一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。

(3)注意:不允许以虚函数作为构造函数。

#include<iostream>using namespace std;class son {public:virtual ~son() {cout << "bye from son" << endl;}};class grandson :public son {public:~grandson() {cout << "bye from grandson" << endl;}};int main() {son* pson = new grandson();delete pson;return 0;}/*Output:bye from grandsonbye from son*/

6.纯虚函数和抽象类

(1)纯虚函数: 没有函数体的虚函数

class A {public:virtual void Print() = 0;};

(2)包含纯虚函数的类叫抽象类。

抽象类只能作为基类来派生新类使用,不能创建抽象类的对象。抽象类的指针和引用可以指向由抽象类派生出来的类的对象。在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

#include<iostream>using namespace std;class A {public:virtual void f() = 0;void g() {this->f();}};class B :public A {void f() {cout << "B:f()" << endl;}};int main() {B b;b.g();return 0;}/*Output:B:f()*/

二、输入输出和模板

输入输出流相关的类

1.类的派生关系:

2.概述:

istream是用于输入的流类,cin就是该类的对象。ostream是用于输出的流类,cout就是该类的对象。iostream是既能用于输入,又能用于输出的类。ifstream是用于从文件读取数据的类ofstream是用于向文件写入数据的类。fstream是既能从文件中读取数据,又能向文件中写入数据的类。

3.标准流对象与重定向:

(1)cerr, clog在参数缺省的情况下,和cout没区别。

(2)cerr和clog的区别:cerr不使用缓冲区,直接向显示器输出信息;而输出到clog中的信息先会被存放在缓冲区,缓冲区满或者刷新时才输入到屏幕。

cin对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据。cout对应于标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写入数据。cerr对应于标准错误输出流,用于向屏幕输出错误信息。clog对应于标准错误输出流,用于向屏幕输出出错信息。

(3)输出重定向

#include<iostream>#include<fstream>using namespace std;int main(){int x, y;cin >> x >> y;freopen("text.txt", "w", stdout); //将标准输出重定向到text.txt文件if(y == 0){ //除数为0,则在屏幕上输出错误的信息。cerr << "error." << endl;}else{cout << x / y << endl; //输出结果到text.txt}return 0;}

(4)输入重定向

#include<iostream>#include<fstream>using namespace std;int main(){double f;int n;freopen("text.txt", "r", stdin); //cin被改为从text.txt中读取数据。cin >> f >> n;cout << f << "," << n << endl;return 0;}

(5)istream类的成员函数:

istream & getline(char * buf, int bufSizse):从输入流中读取bufZise - 1个字符到缓冲区buf,或读到碰到'\n'为止(哪个先到算哪个)。istream &getline(char * buf, int bufSize, char delim):从输入流中读取bufSize - 1个字符到缓冲区buf,或是读到碰到delim字符为止(那个先到算哪个)

注:

需要指出的是,两个函数都会在buf中读入数据的结尾添加'\0',而'\n'和delim都不会被读入到buf,但会被从输入流中取走。如果输入流中'\n'或delim之前的字符个数达到或超过了bufSize个,就会导致读入出错,其结果就是:虽然本次读入已经完成,但是之后的读入就都会失败了。

可以用 if( !cin.getline(...) )判断输入是否结束。

bool eof():判断输入流是否结束。int peek():返回下一个字符,但不从流中去掉。istream & putback(char ch):把字符 ch 放会输入流;istream & ignore(int nCount = 1, int delim = EOF):从流中删掉最多nCount个字符,遇到 EOF 就结束。

(6)看一个易错实例:

用流操纵算子控制输出格式

1.简介:

整数流的基数:流操纵算子dec(十进制), oct(八进制), hex(十六进制), setbase(任意进制)浮点数的精度:浮点数的精度(precision, setprecision)设置域宽(setw, width)使用流操纵算子需要 #include<iomanip>

2.整数流的基数

(1)整数流的基数一旦设置好的话是一直起作用,直到下次改变。

int n = 10;cout << n << endl;cout << hex << n << endl;cout << dec << n << endl;cout << otc << n << endl;/*Output:10a1012*/

3.控制浮点数精度的流操纵算子:

precision是成员函数,其调用方式为:cout << precision(5)setprecision是流操纵算子,其调用方式为:cout << setprecision(5);

(1)setprecision是流操纵算子,是可以连续输出的,即连续起作用,设置一次之后,后面输出浮点数都是按照这个。

(2)上面连个的功能相同。在非定点方式输出时,指定输出浮点数的有效位数;在定点方式输出时,指定输出浮点数的小数点后的有效位数。参数缺省的情况下为非定点方式。

(3)定点方式:小数点必须出现在个位数后面;非定点方式:小数点不一定出现在个位数后面(如科学记数法)。其实定点,顾名思义,就是小数点位置固定。

#include<iostream>#include<iomanip>using namespace std;int main(){double x = 1234567.89, y = 12.34567;int n = 1234567;int m = 12;//(1)参数缺省,为非定点方式。cout << setprecision(6) << x << endl<< y << endl << n << endl << m << endl;//x的位数超过6位,输出的是科学记数法。//m和n是整数,不受上述流操纵算子的影响。cout << "*******************************\n";//(2)设置定点输出的参数:cout << setiosflags(ios::fixed) << setprecision(6) << x << endl<< y << endl << n << endl << m << endl;cout << "*******************************\n";//(3)取消定点输出:cout << resetiosflags(ios::fixed) << x << endl << y << endl;}/*Input:1234567.89 12.34567 1234567 12Output:1.23457e+00612.3457123456712*******************************1234567.89000012.345670123456712*******************************1.23457e+00612.3457*/

4.设置域宽的流操纵算子

(1)设置域宽的(width, setw),两者功能相同,width是成员函数,setw是流操作算子,调用方式不同:

(2)cin.width(4)或 cin >> setw(5)

(3)cout.width(4)或 cout << setw(5)

(4)宽度设置有效性是一次性的,在每次读入和输出之前都要设置宽度。

#include<iostream>using namespace std;int main(){int w = 5;char s[10];cin.width(5);while(cin >> s){/*(1)每次都要设置一遍宽度,因为是有效性是一次性的。(2)输入操作提取字符串的最大宽度比定义的域宽小1这是因为在输入的字符串后面必须加上'\0'。(3)输出时,宽度不足会在前面补空格。*/cout.width(w++);cout << s << endl;cin.width(5);}return 0;}/*Input:1234567890Output:1234567890*/

(5)看一个例子,这个例子较为重要,因为这个例子列举了常用的输出操作。

#include<iostream>#include<iomanip>using namespace std;int main(){int n = 141; double x = 1234567.89, y = 12.34567;//(1)分别以十六进制、十进制、八进制先后输出ncout << "(1)" << hex << n << ' ' << dec << n << ' ' << oct << n << endl;//(2)保留5位有效数字cout << "(2)" << setprecision(5) << x << ' ' << y << endl;//(3)保留小数点后面5位cout << "(3)" << fixed << setprecision(5) << x << ' ' << y << endl;//(4)科学计数法输出,且保留小数点后面5位cout << "(4)" << scientific << setprecision(5) << x << ' ' << y << endl;//(5)非负数要显示正号,输出宽度为12字符,宽度不足则用'*'填补//setfill()不是一次性的,是多次有效的。cout << "(5)" << showpos << fixed << setw(12) << setfill('*') << 12.1 << endl;//(6)非负数不显示正号,输出宽度为12字符,宽度不足把字符放在左边,剩余部分有填充字符填充cout << "(6)" << noshowpos << setw(12) << left << 12.1 << endl;//(7)输出宽度为12字符,宽度不足则把字符放在右边,剩余部分用填充字符填充cout << "(7)" << setw(12) << right << 12.1 << endl;//(8)宽度不足时,负号和数值分列左右,中间用填充字符填充cout << "(8)" << setw(12) << internal << -12.1 << endl;//(9)设置域宽的流操纵算子效果是一次性的。cout << "(9)" << 12.1 << endl;return 0;}/*Output:(1)8d 141 215(2)1.2346e+006 12.346(3)1234567.89000 12.34567(4)1.23457e+006 1.23457e+001(5)***+12.10000(6)12.10000****(7)****12.10000(8)-***12.10000(9)12.10000*/

5.用户自定义的流操纵算子

(1)iostream 里对 << 进行了重载(成员函数),该函数内部会调用p所指向的函数,且以 *this 作为参数 hex 、dec 、oct 都是函数。

ostream & operator << (ostream & (*p)(ostream &));

(2)举个栗子:

ostream & tab(ostream & output){return output << '\t';}cout << "aa" << tab << "bb" << endl;/*Output:aa bb*/

文件读写

1.创建文件

(1)创建文件方法

#include <fstream> // 包含头文件ofstream outFile(“clients.dat”, ios::out|ios::binary);

ios::out 输出到文件, 删除原有内容ios::app 输出到文件, 保留原有内容,总是在尾部添加ios::binary 以二进制文件格式打开文件

(2)也可以先创建ofstream对象,再用 open函数打开

ofstream fout;fout.open("test.out",ios::out|ios::binary);

(3)判断打开是否成功:

if(!fout){cout << “File open error!”<<endl;}

(4)文件名可以给出绝对路径,也可以给相对路径。没有交代路径信息,就是在当前文件夹下找文件。

(5)绝对路径:"c:\\tmp\\mydir\\some.txt"

(6)相对路径:

"\\tmp\\mydir\\some.txt":当前盘符的根目录下的tmp\dir\some.txt"tmp\\mydir\\some.txt":当前文件夹的tmp子文件夹里面的….."..\\tmp\\mydir\\some.txt":当前文件夹的父文件夹下面的tmp子文件夹里面的….."..\\..\\tmp\\mydir\\some.txt":当前文件夹的父文件夹的父文件夹下面的tmp子文件夹里面的…..

函数模板

(1)在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数调用语句。

先找参数完全匹配的普通函数(而非有模板实例化而得的函数)。再找参数完全匹配的模板函数。再找实参经过自动类型转换后能够匹配的普通函数。上面的都找不到,则报错(即编译器不会去自动类型转换然后去匹配模板函数)。

(2)解释一下上面第三句,假如定义了double func(double x),然而调用的时候是func(5),这叫自动类型转换。

(3)函数模板可以重载,只要它们的形参表或类型参数表不同就行

#include<iostream>using namespace std;template<class T1, class T2> //这个叫类型参数表void print(T1 arg1, T2 arg2) //这个叫形参表{cout << arg1 << " " << arg2 << endl;}template<class T>void print(T arg1, T arg2){cout << arg1 << " " << arg2 << endl;}template<class T, class T2>void print(T arg1, T arg2){cout << arg1 << " " << arg2 << endl;}/*不过仍要注意的是,不管是形参表还是类型参数表,都不会以参数的名字去识别是否是重载函数。1和2:类型参数表和形参表都不同1和3:形参表不同2和3:类型参数表不同。*/

(4)函数模版示例:Map

#include<iostream>using namespace std;template<class T, class Pred>void Map(T s, T e, T x, Pred op){for(; s != e; ++s, ++x){*x = op(*s);}}int Cube(int x){return x * x * x;}int a[5] = {1, 2, 3, 4, 5}, b[5];int main(){Map(a, a + 5, b, Cube);for(int i = 0; i < 5; i++){cout << b[i] << endl;}//看到了吧,函数后面不加括号是函数指针!原来如此。return 0;}/*实例化如下函数:void Map(int * s, int * e, int * x, int(*op)(int)){for(; s != e; ++s, ++x){*x = op(*s);}}*/

类模板

1.类模板概述

(1)为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模板生成不同的类。

(2)两种模型:

template <class 类型参数1,class 类型参数2,……> //类型参数表class 类模板名{//成员函数和成员变量};

template <typename 类型参数1,typename 类型参数2,……>//类型参数表class 类模板名{//成员函数和成员变量};

(3)举个栗子:

#include<iostream>using namespace std;template<class T1, class T2>class Pair {public:T1 key;T2 value;Pair(T1 k, T2 v) :key(k), value(v) {};bool operator < (const Pair<T1, T2>& p) const;};template<class T1, class T2>bool Pair<T1, T2>::operator < (const Pair<T1, T2>& p) const {return key < p.key;}int main() {Pair<string, int> student("Tom", 19);cout << student.key << " " << student.value;return 0;}

(4)编译器由类模板生成类的过程叫类模板的实例化。有类模板实例化得到的类,叫模板类。

(5)同一个类模板的两个模板类是不兼容的,比如说,Pair<string, int>a和 Pair<string, double> b,a和 b是没有什么关系的。

2.函数模板作为类模板成员

看个例子:

#include<iostream>using namespace std;template<class T>class A {public:template<class T2>void Func(T2 t) { cout << t; } //成员函数模板};int main() {A<int> a;a.Func('K'); //成员函数模板 Func被实例化a.Func("hello"); //成员函数模板 Func再次被实例化。return 0;}

3.类模板和非类型参数

类模板的类型参数表中可以出现非类型参数:

#include<iostream>using namespace std;template<class T, int size>class CArray {T array[size];public:void Print() {for (int i = 0; i < size; i++) {cout << array[i] << endl;}}};CArray<double, 40> a2;CArray<int, 50> a3;

类模板与派生,类模板与友元,类模板与静态成员变量

1.类模板与派生

(1)类模板从类模板派生

#include<iostream>using namespace std;template<class T1, class T2>class A {T1 v1; T2 v2;};template<class T1, class T2>class B :public A<T2, T1> {T1 v3; T2 v4;};template<class T>class C :public B<T, T> {T v5;};int main() {B<int, double> obj1;C<int> obj2;return 0;}

实例化出的是这两个类 :

class B<int, double> :public A<double, int> {int v3, double v4;};class A<double, int> {double v1, int v2;};

(2)类模板从模板类派生

#include<iostream>using namespace std;template<class T1, class T2>class A {T1 v1; T2 v2;};template<class T>class B :public A<int, double> {T v;};int main() {B<char> obj1; //自动生成两个模板类 :A<int,double> 和 B<char>return 0;}

(3)类模板从普通类派生

#include<iostream>using namespace std;class A {int v1;};template <class T>class B :public A { //所有从B实例化得到的类 ,都以A为基类T v;};int main() {B<char> obj;return 0;}

(4)普通类从模板类派生

#include<iostream>using namespace std;template <class T>class A {T v1;int n;};class B :public A<int> {double v;};int main() {B obj1;return 0;}

2.类模板与友元

(1)函数、类、类的成员函数作为类模板的友元

#include<iostream>using namespace std;void Func1(){}class A{};class B {public:void Func(){}};template<class T>class Tmp1 {friend void Func1();friend class A;friend void B::Func();};//任何从Tmp1实例化来的类 ,都有以上三个友元

(2)函数模板作为类模板的友元

#include<iostream>#include<string>using namespace std;template<class T1, class T2>class Pair {private:T1 key;T2 value;public:Pair(T1 k, T2 v):key(k),value(v){}bool operator <(const Pair<T1, T2>& p) const;template<class T3, class T4>friend ostream & operator<<(ostream& o, const Pair<T3, T4>& p);};template<class T1, class T2>bool Pair<T1, T2>::operator<(const Pair<T1, T2>& p)const {return key < p.key;}template<class T1, class T2>ostream & operator << (ostream& o, const Pair<T1, T2>& p) {o << "(" << p.key << "," << p.value << ")";return o;}int main() {Pair<string, int> student("Tom", 29);Pair<int, double> obj(12, 3.14);cout << student << " " << obj;return 0;}/*任意从 template <class T1,class T2>ostream & operator<< (ostream & o,const Pair<T1,T2> & p)生成的函数,都是任意Pair模板类的友元*/

(3)函数模板作为类的友元

#include<iostream>using namespace std;class A {int v;public:A(int n) :v(n){}template<class T>friend void Print(const T& p) {cout << p.v;}};int main() {A a(4);Print(a);return 0;}/*所有从 template <class T>void Print(const T & p)生成的函数,都成为 A 的友元但是自己写的函数void Print(int a) { }不会成为A的友元*/

(4)类模板作为类模板的友元

#include<iostream>using namespace std;template<class T>class B {T v;public:B(T n):v(n){}template<class T2>friend class A;};template<class T>class A {public:void Func() {B<int> o(10);cout << o.v << endl;}};int main() {A<double> a;a.Func();return 0;}/*A< double>类,成了B<int>类的友元。任何从A模版实例化出来的类,都是任何B实例化出来的类的友元*/

3.类模板与静态成员变量

(1)类模板与static成员:类模板中可以定义静态成员 ,那么从该类模板实例化得到的所有类 ,都包含同样的静态成员 。

#include<iostream>using namespace std;template<class T>class A {private:static int count;public:A() { count++; }~A() { count--; }A(A&) { count++; }static void PrintCount() { cout << count << endl; }};//静态成员变量一定要初始化一下,实例化出来的模板类按照下面这个怪怪的格式。template<> int A<int>::count = 0;template<> int A<double>::count = 0;int main() {A<int> ia;A<double> da;ia.PrintCount();da.PrintCount();return 0;}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。