C++:从0到1

简单认识(从C到C++)

C++是在C语言的基础上创建的一门功能更为强大的面向对象程序设计语言。面向对象语言把所有属性和行为封装在类里,然后由各种类来完成程序的编写和执行。面向对象语言特征:抽象,封装,继承,多态。

函数指针

每个函数都占用一段连续的内存空间,函数指针就是指向函数起始地址的指针。

int (*pf)(int, char);  //pf就是一个函数指针
pf(1,'c'); //调用方式

函数指针可用于对任意类型的数组进行排序,最后传入比较函数即可。

void qsort(void *base, int enlem, unsigned int width, int (*pfCompare)(const void *, const void *));

位运算

&  // 按位与(数据二进制按位进行与操作,有0,则为0,否则为1)
// 将int类型变量n的低8位全置为0,而其余位不变。
n = n & 0xffffff00;
// 也可以写成
n &= 0xffffff00;
// 如果n是short类型
n &= 0xff00;
// 判断int类型变量n的第7位是否是1?(从右往左数)
n & 0x80 // 判断这个表达式即可 0x80: 1000 0000

===============================================

|  // 按位或 (有1,则为1,否则为0)
// 将int类型变量n的低8位全置为1,而其余位不变。
n |= 0xff; // 0xff: 1111 1111

===============================================

^  // 按位异或 (不相同则为1,相同则为0)
// 将int类型变量n的低8位取反,而其余位不变。
n ^= 0xff;
// 运算特点(加密,解密)
a^b=c; c^b=a; c^a=b;
// 不通过临时变量,交换两个变量的值
int a = 5, b = 7;
a = a ^ b;
b = b ^ a;
a = a ^ b;

===============================================

~  // 按位非 (1变0,0变1)

===============================================

<< // 左移(各二进制为全部向左移动n位,高位丢弃,低位补0)
a << b;
// 实际上,左移1位,相当于乘以2,左移n为,就等于乘以 2的n次方,而左移操作比乘法操作快得多。

>> //右移 (各二进制为全部向左移动n位,符号位为1,补1,符号位为0,补0)
a >> b;
// 实际上,右移n位,就相当于操作数 除以 2的n次方,并且将结果往小里取整
-25 >> 4 = -2
-2 >> 4 = -1
18 >> 4 = 1

引用

类型名 & 引用名 = 某变量名
某个变量的引用,等价于这个变量,相当于该变量的一个别名

int n = 4;
int & r = n;  // r引用了n,r的类型是 int &

特征:

  • 定义引用时,一定要将其初始化成引用某个变量
  • 初始化后,它就一直引用该变量,不会再引用别的变量了
  • 引用只能引用变量,不能引用常量和表达式

应用:两个变量的交换

C实现

void swap(int *a, int *b) {
	int temp;
	temp = *a; *a = *b; *b = temp;
}

int n1, n2;
swap(&n1, &n2);

C++实现(有了C++的引用)

void swap(int &a, int &b) {
	int temp;
	temp = a; a = b; b = temp;
}
int n1, n2;
swap(n1, n2); // n1, n2的值被交换

const关键字和常量

定义常量,代替宏定义

const int MAX_VAL = 23;
const double Pi = 3.14;
const char * SCHOOL_NAME = "Peking University";

定义常量指针

  1. 不可通过常量指针修改其指向的内容
int n, m;
const int *p = &n;
*p = 5;  // 编译报错
n = 4; // ok
p = &m; // ok,常量指针的指向可以变化
  1. 不能把常量指针赋值给非常量指针,反过来可以
const int *p1; int *p2;
p1 = p2; // ok
p2 = p1; // error,因为可以通过指针p2修改其指向的内容,而常量指针不可,避免歧义,所以这里不允许这么赋值
p2 = (int *)p1; // ok,强制类型转换
  1. 函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容
void MyPrintf(const char *p) {
	strcpy(p, "this"); //编译出错,编译器是如何发现的呢?是因为strcpy第一个参数是 char*,而p是 const常量指针,因为第二条《不能把常量指针赋值给非常量指针》,所以不匹配,报错
	printf("%s", p);
}

动态内存分配

int *p = new int;  // 动态分配出来使用 new
p = 5
delete p;  // 释放动态分配的内存,delete 必须指向 new出来的

int *p = new int[20];  // 动态分配数组
p[0] = 1;
delete []p;

内联函数

函数调用是有时间开销的。为了减少函数调用的开销,引入了内联函数。在编译时,编译器会将内联函数的代码插入到调用语句处,从而不会产生调用函数语句,但是会由于copy了很多份代码,导致可执行文件变大

使用 inline 关键字定义一个内联函数

inline int Max(int a, int b) {
	if(a > b) return a;
	return b;
}

重载函数

一个或多个函数,名字相同,然而参数个数或参数类型不相同,叫做函数的重载,其使得函数的命名变得简单

int Max(double f1, double f2) {}
int Max(int n1, int n2) {}
int Max(int n1, int n2, int n3) {}

Max(3.4, 2.5); // 调用1
Max(1, 2); // 调用2
Max(1, 2, 3); // 调用3
Max(1, 2.4); // 编译error, 二义性

编译器根据调用语句实参的个数和类型判断调用哪个函数

函数缺省参数

定义函数可以让其最右边的连续若干个参数有缺省值,那么调用函数时,相应的位置不写参数,参数就是缺省值

void func(int a, int b = 2, int c = 3) {}
func(10); // 等效于 func(10, 2, 3)
func(10, 8); // 等效于 func(10, 8, 3)
func(10, , 8); // 报错,只能最右边连续若干个

缺省参数的作用并不是为了少些一个参数而已。主要是为了提高程序的可扩展性。
比如现在有一个函数,有一个参数,在很多地方都调用了这个函数,随着业务的发展,这个函数需要额外加一个参数来单独在一两个地方使用,这是就可以用缺省参数,给一个默认值(也就是缺省值),其他调用不变(避免所有调用的地方都需要修改),新调用的地方传入自己应该的参数即可。

类和对象

内联成员函数

由于上面对其概念已大概讲述,这里不再叙述。

实现方式有两种:

  • inline + 成员函数
  • 整个函数体出现在类定义内部
class B {
	inline void func1();
	void func2() {
	};
}
void B::func1() {}; // 表示func1的定义是属于类B的,使用 B::

重载成员函数

class Location {
	private:
		int x, y;
	public:
		// 函数有缺省值
		void init(int x = 0, int y = 0);
		// valueX 成员函数重载
		void valueX(int value) { x = value;}
		int valueX() {return x;}
};

void Location::init(int X, int Y) {
	x = X;
	y = Y;
}

int main() {
	Location A;
	A.init(5);  // 传入一个参数x,则x = 5,y值使用缺省值
	A.valueX(6); // 设置 x = 6
	cout << A.valueX() << endl;  // 输出 x的值为6
	return 0;
}

注意:使用缺省参数要注意避免有重载函数时的二义性。
比如如果对上面函数添加如下缺省值的修改,那么调用的时候就编译错误

void valueX(int value = 0) { x = value;}
A.valueX(); // 编译出错,编译器无法判断调用哪个 valueX

构造函数

  • 名字与类名相同,可以有参数,不能有返回值(void也不行)
  • 内存空间并不是其分配的,进入构造函数以后,内存空间已分配好,仅仅是对对象进行初始化工作,如给成员变量赋初始值
  • 如果定义类没写构造函数,则编译器会生成一个默认的无参的构造函数,不过其不做任何操作
  • 对象生成时,构造函数自动被调用。对象一生成,就再也不能在其上执行构造函数
  • 一个类可以有多个构造函数

为什么要有构造函数呢?

  • 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
  • 有时对象没被初始化就是还使用,会导致程序出错。
class Complex {
	private:
		double real, imag;
	public:
		void Set(double r, double i);
}; // 编译器会自动生成默认无参构造函数
Complex c1; // 默认构造函数会被调用
Complex *pc = new Complex; // 默认构造函数会被调用

自定义构造函数

class Complex {
	private:
		double real, imag;
	public:
		Complex(double r, double i = 0); // 自定义构造函数,系统就不会生成默认的了
}
Complex::Complex(double r, double i) {
	real = r; imag = i;
}
Complex c1; // error,缺少构造函数的参数
Complex *pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex *pc = new Complex(3,4); // 动态生成一个对象

构造函数可以有多个,也就是构造函数的重载

复制构造函数

  • 只要一个参数,即对同类对象的引用
  • 形如 X::X(X &)X::X(const X &),二者只能选一,否则就编译错误
  • 没有定义,编译器自动生成,完成复制功能
class Complex {
	public: 
		double real,imag;
	Complex() {};
	Complex(const Complex &c) {
		real = c.real;
		imag = c.imag;
		cout << "copy constructor called";
	}
};
Complex c1;
Complex c2(c1); // 调用自己定义的复制构造函数

类型转换构造函数

  • 实现类型的自动转换,编译系统会自动调用,转换构造函数
  • 只有一个参数,且不是复制构造函数
  • 自动转换期间会建立一个 临时对象或者临时变量
class Complex {
	public:
		double real, imag;
		Complex(int i) { // 类型转换构造函数
			real = i, imag = 0;
		}
		Complex(double r, double i) {
			real = i, imag = 0;
		}
}
int main() {
	Complex c1(7, 8);
	Complex c2 = 12; // 调用了类型转换函数,是一个初始化语句,不生产临时对象,生成的对象就是c2
	c1 = 9; //调用了类型转换函数,其是一个赋值语句,9被自动转换成一个临时Complex对象
	return 0;
}

析构函数

  • 对象消亡时,会被自动调用,在这个函数里做消亡相关的工作(如释放动态分配的空间)
  • 名字和类名相同,不过在前面加“~”
  • 没有参数和返回值,一个类型最多只有一个析构函数
  • 没有写,编译器会自动生成,写了,就不会自动生成
  • 手动调用delete也会触发自动调用析构函数
class String {
	private:
		char *p;
	public:
		String() {
			p = new char[10];
		}
		~String();
}
String::~String() {
	delete []p;
}

静态成员变量和静态成员函数

在说明前面添加 static 关键字

class CRectangle {
	private:
		int w, h;
		static int nTotalArea; // 静态成员变量
		static int nTotalNumber;
	public:
		CRectangle(int w_, int h_);
		~CRectangle();
		static void PrintTotal(); // 静态成员函数
}

静态成员变量就一份,为所有对象共享,其相当于全局变量,不需要依赖对象生成而生成,那为什么不定义成全局的呢,因为其一般是为某个类而生,其他类不允许直接调用它,为了整体性和安全性,将某个类的全局变量定义成这个类的静态变量即可。
sizeof 运算符不会计算静态成员变量。
访问方式:

CRectangle::PrintTotal(); // 类名::成员名
CRectangle r; r.PrintTotal(); // 这里只是一个调用形式,其还是作用于类本身,而不是对象 r

成员对象和封闭类的概念

封闭类:含有其他对象作为其成员变量的类就成为封闭类

// 轮胎类
class CTyre {
	private:
		int radius, width;
	public:
		// 此种方式叫做初始化列表。等同于 radius = r, width = w;
		CTyre(int r, int w):radius(r),width(w){};
};
// 引擎类
class CEngine {
};
// 汽车类, 也就是 封闭类
class CCar {
	private: 
		int price;
		CTyre tyre;
		CEngine engine;
	public:
		CCar(int p, int tr, int tw);
};

CCar::CCar(int p, int tr, int tw):price(p), tyre(tr, tw) {}; // 初始化列表方式

int main() {
	CCar car(20000, 17, 225);
	return 0;
}

注意:这里的CCar必须要自己去写构造函数,因为其有一个成员对象 tyre,CTyre自定义了构造函数,其需要传入r和w,如果CCar不写构造函数,使用编译器自动生成的默认构造函数是无参数的,这样就没办法初始化tyre了,所以就好报错。而CEngine在这里是无影响的。

CCar封闭类对象生成时,自身和成员对象的构造函数和析构函数的调用顺序如下(跟声明的顺序有关):

  1. CTyre 构造函数
  2. CEngine 构造函数
  3. CCar 构造函数
  4. CCar 析构函数
  5. CEngine 析构函数
  6. CTyre 析构函数

友元

friend
友元有友元函数和友元类,存在的目的是为了让其他成员函数或者全局函数有机会去可以访问一个类的私有成员变量或对象。
一个类的友元函数可以访问该类的私有成员

class CCar; // 提前声明
class CDriver {
	public:
		void ModifyCar(CCar *pCar); // 改装汽车
};
class CCar {
	private:
		int price;
	friend int MostExpensiveCar(CCar cars[], int total); // 声明全局友元函数
	friend void CDriver::ModifyCar(CCar *pCar); // 声明 Driver类的友元函数
};
void CDriver::ModifyCar(CCar *pCar) {
	pCar->price += 1000; // 改装后价格增加
}
// 求最贵汽车的价格
int MostExpensiveCar(CCar cars[], int total) {
	int tempMax = -1;
	for(int i = 0; i < total; i++) {
		if(cars[i].price > tempMax) {
			tempMax = cars[i].price;
		}
	}
	return tempMax;
}

A是B的友元类,那么A的成员函数可以访问B的私有成员

  • 友元类之间不可传递和继承(A是B的友元,B是C的友元,但是A不是C的友元)
class CDriver; // 提前声明
class CCar {
	private:
		int price;
	friend class CDriver; // 声明CDriver为友元类
}
class CDriver {
	public:
		CCar myCar;
		void ModifyCar() {
			myCar.price += 1000; // CDriver是CCar的友元类,所以可以访问其私有成员
		}
}

this指针

其作用就是指向成员函数所作用的对象。
C++程序到C程序的翻译
C++程序

class CCar {
	public:
		int price;
		void SetPrice(int p);
};
void CCar::SetPrice(int p) {
	price = p;
}
int main() {
	CCar car;
	car.SetPrice(20000);
	return 0;
}

对应的C程序
C++的类对应C的结构体,成员变量对应变量,成员函数对应全局函数,全局函数会多一个参数 this指针,代表这个函数指向哪个对象做修改。

struct CCar {
	int price;
};
void SetPrice(struct CCar *this, int p) {
	this->price = p;
}
int main() {
	struct CCar car;
	SetPrice(&car, 20000);
	return 0;
}

this指针作用

class A {
		int i;
	public:
		void Hello() {cout << "hello" << endl;}
}; // void Hello(A *this) {cout << "hello" << endl;}
int main() {
	A *p = NULL;
	p->Hello(); // 这里是可以正常输出hello的,因为作用的对象是NULL,但是在函数内并没有使用该对象,如果里面有使用对象的i成员变量,就会编译错误
	// Hello(p);
}

常量对象、常量成员函数和常引用

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

class Demo {
	private:
		int value;
	public:
		void SetValue() {}
};
const Demo Obj; // 常量对象

成员函数后面加const,则该成员函数为常量成员函数,其定义不应修改其所作用的对象。
不能修改成员变量(静态成员变量除外)
不能调用同类的非常量成员函数(静态成员函数除外,因为成员函数其内部可能会修改成员变量)

class Sample {
	public:
		int value;
		void GetValue() const;
		void func() {};
		Sample() {};
};
void Sample::GetValue() const {
	value = 0; // 编译error
	func(); // 编译error
}

两个成员函数,名字和参数都一样,但是一个是const,一个不是,算重载,而非重定义

引用前面加const 成为常引用,传入的常引用对象,不能在内部修改其值。也就是为了避免传入的对象被修改导致外面的也修改了,所以可以使用常引用避免这种情况。

运算符重载

基本概念

  • 对抽象数据(也就是类生成的对象)类型也能直接使用C++提供的运算符
  • 运算符重载的实质是函数重载
返回值类型 operator 运算符(形参表){
	....
}

程序编译时

  • 把含有运算符表达式 对应 转换成 运算符函数的调用
  • 把 运算符的操作数 转换成 运算符函数的 参数

重载为普通函数(参数个数为运算符目数)

Complex operator+(const Complex &a, const Complex &b) {
	return Complex(a.real + b.real, a.imag + b.imag);
}

Complex a(1,2), b(2,3), c;
c = a + b; // 相当于 c = operator+(a, b);

重载为成员函数(参数个数为运算符目数减一)

class Complex {
	public:
		Complex(double r = 0.0, double m = 0.0):real(r), imag(m) {}
		Complex operator+(const Complex &);
		Complex operator-(const Complex &);
	private:
		double real;
		double imag;
};
Complex Complex::operator+(const Complex & operand2) {
	return Complex(real + operand2.real, imag + operand2.imag);
}
Complex Complex::operator-(const Complex & operand2) {
	return Complex(real - operand2.real, imag - operand2.imag);
}

int main() {
	Complex x, y(4.3, 8.2), z(3.3, 1.1);
	x = y + z; // x = y.operator+(z);
	x = y - z; // x = y.operator-(z);
	return 0;
}

重载赋值运算符

  • “=”的重载
  • 只能重载为 成员函数
    这里有一个浅复制和深复制的概念。因为系统的默认的复制构造函数是一个浅复制,所以一般都需要重写
    浅复制:就是两个指针指向了同一片内存空间,两个指针析构的时候就会多释放一次,就会报错,且原先指针指向的空间就成了垃圾内存,不会自行释放
    深复制:初始化一块新内存空间,然后从原空间复制内容过来,两块区域互不影响

实例 - 长度可变的整型数组类

class CArray {
	int size;  // 数组元素个数
	int *ptr;  // 指向动态分配的数组
public:
	CArray(int s = 0);
	CArray(CArray &a);
	~CArray();
	void push_back(int v);
	CArray & operator= (const CArray &a);
	int lenght() {return size;}
	int& CArray::operator[](int i) {
		// n = a[i]; a[i] = 4;
		return ptr[i];
	}
	CArray::CArray(int s):size(s) {
		if (s == 0) {
			ptr = NULL;
		} else {
			ptr = new int[s];
		}
	}
	CArray::CArray(CArray &a) {
		if(!a.ptr) {
			ptr = NULL;
			size = 0;
			return;
		}
		ptr = new int[a.size];
		memcpy(ptr, a.ptr, sizeof(int) * a.size);
		size = a.size;
	}
	CArray::~CArray() {
		if(ptr) delete []ptr;
	}
	CArray & CArray::operator= (const CArray &a) {
		if(ptr == a.ptr) {
			return *this;
		}
		if(a.ptr == NULL) {
			if (ptr) delete []ptr;
			ptr = NULL;
			size = 0;
			return *this;
		}
		if(size < a.size) {
			if(ptr) delete []ptr;
			ptr = new int[a.size];
		}
		memcpy(ptr, a.ptr.sizeof(int) * a.size);
		size = a.size;
		return *this;
	}
	void CArray::push_back(int v) {
		// 内存空间可以多分配一点,减少重新分配的次数,提高效率
		if(ptr) {
			int *tempPtr = new int[size+1];
			memcpy(tempPtr, ptr.sizeof(int) * size);
			delete []ptr;
			ptr = tempPtr;
		} else {
			ptr = new int[1];
		}
		ptr[size++] = v;
	}
};

继承与派生

基本概念

  • 派生类还有基类所有的特点,也可以对其进行修改和扩充
  • 独立使用
  • 派生类继承了基类的private成员,依然不能访问基类的private成员
  • 派生类对象包含基类对象的,存储空间等于基类的空间加上自己的空间,而且基类对象的成员变量存储位置在派生类之前的
// 基类
class CStudent {
	private:
		string sName;
		int nAge;
	public:
		bool IsThreeGood() {};
		void SetName(const string &name) {
			sName = name;
		}
};
// 派生类写法  类名:public 基类名
class CUndergraduateStudent: public CStudent {
	private:
		int nDepartment;
	public:
		bool IsThreeGood() {}; // 覆盖
		bool CanBaoYan() {};
};

class CGraduateStudent: public CStudent {
	private:
		int nDepartment;
		char szMentorName[20];
	public:
		int CountSalary() {};
};

复合关系和继承关系

复合:A类中有一个成员变量k,k是类B的对象。A和B是复合关系。比如:点类和 圆类,圆类有个中心点,所以圆类有个点类的对象成员变量
继承:A类,B是A的派生类。逻辑上来说,B对象是一个A对象。比如:女人不能继承自男人,而应该有个基类“人”,女人和男人派生自“人”。

基类/派生类同名成员和protected访问范围说明符

class base {
		int j;
	public:
		int i;
		void func();
		
};
class derived: public base {
	public:
		int i;
		void access();
		void func();
};
void derived:access() {
	j = 5; // error,派生类无法访问父类的私有成员
	i = 5; // 引用的是派生类的i
	base::i = 5; // 引用的是基类的 i
	func(); // 派生类的
	base::func(); // 基类的
}
derived obj;
obj.i = 1;
obj.base::i = 1;

一般来说,我们不建议子类和父类有同名的成员变量的
基类的protected成员可以被派生类的成员函数访问到

class Father {
	private: int nPrivate; // 私有成员
	public: int nPublic; // 公有成员
	protected: int nProtected; // 保护成员
};
class Son: public Father {
	void AccessFather() {
		nPublic = 1; // OK
		nPrivate = 1; // error
		nProtected = 1; // OK,访问从基类继承的protected成员
		Son f;
		f.nProtected = 1; // error, f不是当前对象
	}
};
int main() {
	Father f;
	Son s;
	f.nPublic = 1; // OK
	s.nPublic = 1; // OK
	f.nProtected = 1; // error
	f.nPrivate = 1; // error
	s.nProtected = 1; // error
	s.nPrivate = 1; // error
	return 0;
}

派生类的构造函数

  • 派生类对象包含基类对象
  • 执行派生类构造函数之前,先执行基类的构造函数(注意,如果没有显式调用基类的构造函数,会自动调用默认的构造函数)
  • 派生类交代基类初始化,具体形式:构造函数名(形参表): 基类名(基类构造函数实参表) {}
  • 其调用顺序跟 封闭类 基本一致
class Bug {
	private:
		int nLegs;
		int nColor;
	public:
		int nType;
		Bug(int legs, int color);
		void PrintBug(){};
};

class FlyBug: public Bug {
		int nWings;
		Skill sk1, sk2;
	public:
		FlyBug(int legs, int color, int wings);
};
class Skill {
	public:
		Skill(int n) {}
};

Bug::Bug(int legs, int color) {
	nLegs = legs;
	nColor = color;
}
// 错误的FlyBug构造
FlyBug::FlyBug(int legs, int color, int wings) {
	nLegs = legs; // error 不能访问
	nColor = color; // error 不能访问
	nType = 1;
	nWings = wings;
}
// 正确的处理方式(使用成员初始化列表的方式)
FlyBug::FlyBug(int legs, int color, int wings):Bug(legs, color), sk1(5), sk2(color) {
	nWings = wings;
}

public继承的赋值兼容规则

直接基类
间接基类

执行构造函数时,从最顶层基类向下依次调用构造函数
执行析构函数时,从自身类开始向上依次调用析构函数

多态与虚函数

基本概念

多态:是面向对象程序设计里非常重要的机制,VB只有继承(基于对象的程序设计语言),没有多态
虚函数:使用关键字 virtual 修饰,来实现多态机制。构造函数和静态成员函数不能是虚函数。

class base {
	virtual int get();
};
int base::get(){}

多态机制

  • 派生类的指针可以赋值给基类指针
  • 通过基类指针调用基类和派生类中的同名虚函数时:
    • 若该指针指向一个基类对象,那么被调用是基类的虚函数
    • 若该指针指向一个派生类对象,那么被调用的是派生类的虚函数

实例

  • 使用多态的游戏程序
  • 几何形体多态

多态实现原理

多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。这叫做动态联编。
动态联编的实现机制:虚函数表。有虚函数的类对象都会多出4个字节,此4个字节就是用来存放虚函数表地址的。
每一个有虚函数的类都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。

多态的函数调用语句被编译成一系列根据基类指针所指向的对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用函数的指令。
所以说,指令在执行过程中,这些对象里面存放的是哪一个类的虚函数表的指针,最终就会跑到哪一个类的虚函数表里面去查找虚函数的地址。

多态语句在运行期间有额外的时间和空间开销
空间:每个对象都有多出4个字节来存放虚函数表地址的引用
时间:执行时,需要查询虚函数表

虚析构函数

问题

class CSon {
	public: 
		~CSon() {}; // 错误方式
		virtual ~CSon() {}; // 正确方式,如果定义为虚函数,那么在delete p的时候,会先调用派生类的虚析构函数,再调用基类的虚析构函数
	
};
class CGrandSon:CSon {
	public: ~CGrandSon() {};
};
int main() {
	CSon *p = new CGrandSon;
	delete p; // 此时p是基类指针,所以说调用的是基类的析构函数,这里就会有问题,派生类对象空间没有被释放。解决方式:将基类的析构函数定义为虚析构函数
	return 0;
}

纯虚函数和抽象类

纯虚函数:没有函数体的虚函数

class A {
	private:
		int a;
	public:
		virtual void Print() = 0; // 纯虚函数
		void fun() { count << "fun"; }
};

抽象类:包含纯虚函数的类

  • 只能作为基类来派生新类使用,也就是不能直接使用
  • 不能创建抽象类的对象
  • 如果一个类从抽象类派生而来,那么它只有实现了基类的所有纯虚函数,才能成为非抽象类,否则它还是一个抽象类
A a; // 错误
A *pa; // OK
pa = new A; // 错误
class A {
	public:
		virtual void f() = 0; // 纯虚函数
		void g() { this->f(); }; // OK
		A() {} // 这里调用 f() 会报错,构造函数中不允许调用虚函数
};
class B: public A {
	public 
		void f() { cout << "B:f()" << endl; }
};
int main() {
	B b;
	b.g(); // 这里会调用 B的 f()
	return 0;
}

模板

泛型程序设计

  • Generic Programming
  • 算法实现时不指定具体要操作的数据的类型
  • 泛型 - 算法实现一遍 -> 适用于多种数据结构
  • 优势:减少重复代码的编写
  • 大量编写模板,使用模板的程序设计

函数模板

template <class 类型参数1, class 类型参数2, ……>
返回值类型 模板名(形参表) { 
	函数体 
}
template <class T>
void Swap(T &x, T &y) {
	T temp = x;
	x = y;
	y = temp;
}
int main() {
	int n = 1, m = 2;
	Swap(n, m); // 编译器自动生成 void Swap(int &, int &) 函数
	double f = 1.2, g = 2.4;
	Swap(f, g); // 编译器自动生成 void Swap(double &, double &) 函数
	return 0;
}

函数模板中可以有不止一种类型参数

template <class T1, class T2>
T2 print(T1 arg1, T2 arg2) {
	cout << arg1 << " " << arg2 << endl;
	return arg2;
}

函数模板可以重载,只要它们的形参表不同即可。

C++编译器遵以下的优先顺序:

  • Step1: 先找 参数完全匹配普通函数 (非由模板实例化而得的函数)
  • Step2: 再找 参数完全匹配模板函数
  • Step3: 再找 实参 经过 自动类型转换 后能够匹配的 普通函数
  • Step4: 上面都找不到,则报错
template <class T>
T Max(T a, T b) {
	cout << "Template Max 1" << endl;
	return 0;
}

template <class T, class T2>
T Max(T a, T2 b) {
	cout << "Template Max 2" << endl;
	return 0;
}

double Max(double a, double b) {
	cout << "MyMax" << endl;
	return 0;
}

int main() {
	int i = 4, j = 5;
	Max(1.2, 3.4); // 调用 Max(double a, double b) 函数
	Max(i, j); // 调用第一个 Max(T a, T b) 模板生成的 Max(int a, int b)函数
	Max(1.2, 3); // 调用第二个 Max(T a, T2 b) 模板生成的 Max(double a,  int b)函数
	return 0;
}

为了赋值兼容原则引起函数模板中类型参数的二义性,可以在函数模板中使用多个参数类型来避免二义性。多个参数可适配性更强,更灵活

类模板

问题提出:
定义一批相似的类 --> 定义类模板 --> 生成不同的类
在调用类模板时,指定参数,由编译系统根据参数提供的数据类型自动产生相应的 模板类

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

// 类模板外定义成员函数方式
template <class 类型参数1, class 类型参数2, ……>
返回值类型 类模板名<class 类型参数1, class 类型参数2, ……>:: 成员函数名(参数表) {
	……
}

Pair类模板

template <class T1, class T2>
class Pair {
	public:
		T1 key;
		T2 value;
		Pair(T1 k, T2 v):key(k), value(v) {};
		bool oprator<(const Pair<T1, T2> &p) const;
};

template<class T1, class T2>
bool Pair<T1, T2>::oprator<(const Pair<T1, T2> &p) const {
	return key < p.key;
}

int main() {
	// 实例化一个类 Pair<string, int>
	Pair<string, int> student("Tom", 19);
	cout << student.key << " " << student.value;
	return 0;
}

类模板实例化后就是一个模板类
类模板中可以包含函数模板
类模板与继承

  • 类模板派生出类模板
  • 模板类派生出类模板
  • 普通类派生出类模板
  • 模板类派生出普通类

标准模板库STL

顺序容器 Vector

List

Deque

函数对象

Set 和 MultiSet

Map 和 MultiMap

容器适配器

C++11新特性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sailip

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值