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;        //引用的是派生类的i
	base::i = 5;  //引用的是基类的i
	func();		  //派生类的成员函数
	base::func(); //基类的成员函数
}
int main() {
	//在类外,规则是类似的。
	derived obj;
	obj.i = 1;       //派生类的i
	obj.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 constructed
Derived constructed
MoreDerived constructed
MoreDerived destructed
Derived destructed
Base 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 grandson
bye 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:
10
a
10
12
*/

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 12

Output:
1.23457e+006
12.3457
1234567
12
*******************************
1234567.890000
12.345670
1234567
12
*******************************
1.23457e+006
12.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:
1234567890

Output:
 1234
  5678
     90
*/

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

#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
    int n = 141; double x = 1234567.89, y = 12.34567;
    //(1)分别以十六进制、十进制、八进制先后输出n
    cout << "(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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值