目录
C++内存分区模型
程序运行前区域:
代码区(共享,只读):存放函数体的二进制代码,由操作系统进行管理(内存中只有一份代码)
全局区:存放全局变量和静态变量以及常量(全局变量、静态变量、static关键字、常量、字符串常量)(const修饰局部常量不在全局区)
程序运行后区域:
栈区:由编译器自动分配释放,存放函数的参数和局部变量
注意:不要返回局部变量地址
堆区(C++中主要用new在堆区开辟内存):由程序员分配和释放,程序结束由操作系统回收
new运算符
int *p = new int (10);//在堆区创建一个int类型数据,该数据为10
delete p;//释放内存
int *arr = new int[10];//创建长度为10的整型数据的数组
delete [] arr;//释放数组
引用
引用:给变量起别名
语法:数据类型 &别名 = 原名
注意:①引用必须初始化(int &b;//是错误的)
②引用一旦初始化后,就不可以更改(不可以改变代表的内存区域,即只能是原名的引用)
int a = 10;
//创建引用
int &b = a;
b = 100;//操作同一块内存,则a也等于100
引用做函数参数
int a = 10;
int b = 20;
//改变两个数的值
void myswap(int &a , int &b)
{
int temp = a;
a = b;
b = temp;
}
//优点:可以简化指针修改实参
引用做函数返回值
①不要返回局部变量引用
②函数调用可以作为左值
int & test()
{
static int a = 20;
return a;
}
int main()
{
int &ref = test();
cout << "ref = "<< ref << endl;//20
test() = 100;//相当于a = 100;
cout << "ref = "<< ref << endl;//100
}
引用的本质
本质:引用的本质在C++内部实现是一个指针常量(指向不可修改,指向内容可以修改)
int a = 10;
//自动转换为 int * const ref = &a;指针常量的指针指向不可改,也说明引用为什么不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为 :*ref = 20;
常量引用
用来修饰形参,防止误操作
void showValue(const int &val)
{
val = 100;//报错
}
int ref & = 10 ;//是错误的
//加上const之后 编译器将代码修改 int temp = 10 ; const int & ref = temp;
const int & ref = 10; //正确的,引用必须引一块合法内存空间
函数高级
函数默认参数
//如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
//如果实参有传入具体值,就用实参值,没有传入具体值就用默认值
//如果函数声明有默认参数,函数实现就不能有默认参数
//声明和实现只能有一个默认参数
int func2(int a ,int b);
int func2(int a = 10 , int b = 20)
{
}
函数占位参数
//函数占位参数,占位参数也可以有默认参数
void func(int a , int )
{
}
函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
同一个作用域下
函数名称相同
函数参数类型不同 或者个数不同 或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
函数重载的注意事项
1、引用作为重载条件
void func(int &a)//int &a = 10; 不合法
{
cout << "func(int &a)调用" << endl;
}
void func(const int &a)//const int &a = 10; 合法
{
cout << "func(const int &a)调用" << endl;
}
int a = 10;
func(a);//调用上面第一个函数
func(10);//调用上面第二个函数
2、函数重载碰到默认参数会出现二义性,所以函数重载,函数尽量不要使用默认参数
类和对象
C++面向对象三大特性为:封装、继承、多态
类可以嵌套
封装
类访问权限有三种:
public 公共权限 成员 类内可以访问 类外可以访问
protected 保护权限 成员 类内可以访问 类外不可以访问 儿子可以访问父亲内容
private 私有权限 成员 类内可以访问 类外不可以访问 儿子不可以访问父亲内容
//类中的属性和行为 统一称为成员
//属性 也叫:成员属性 成员变量
//行为 也叫:成员函数 成员方法
double PI = 3.14;
//class 代表设计一个圆类,类后面紧跟着的就是类的名称
class Circle
{
//访问权限
//公共权限
public:
//属性
//半径
int m_r;
//行为
//获取圆的周长
double calculateCZ()
{
return 2*PI*m_r;
}
}
int main()
{
//通过圆类,创建具体的圆(对象)
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
//输出圆的周长
cout << "圆的周长为:" << c1.calculateCZ() << endl;
}
struct和class区别
区别:默认访问权限不同
struct默认权限为公共
class默认权限为私有
成员属性设置为私有(尽量都设置为私有)
优点1:可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
//设计人类
class Person
{
//可以通过公共权限接口,在main函数中进行读取或设置
public:
//设置年龄
void setAge(int age)
{
if(age > 150 || age < 0)
{
return 0;
}
m_Age = age;
}
//获取年龄
int getAge()
{
return m_Age;
}
private:
//年龄
int m_Age;
};
对象的初始化和清理
构造函数和析构函数
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前面加上符号~
- 构造函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无须手动调用,而且只会调用一次
构造函数的分类和调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方法:
class Person;
//括号法
Person p1;//默认构造函数调用
Person p2(10);//有参构造函数调用
Person p3(p2);//拷贝构造函数调用
//注意事项1
//调用默认构造函数时候,不要加()
//因为下面这行代码,编译器会认为是一个函数声明,不会认为在创建对象
Person p1();
//显示法
Person p1;
Person p2 = Person(10);//有参构造
Person p3 = Person(p2);//拷贝构造
Person(10);//匿名对象 特点:当前执行结束后,系统会立即回收掉匿名对象(即会调用析构函数)
//注意事项2
//不要利用拷贝构造函数初始化匿名对象,编译器会认为Person (p3) === Person p3;会报重定义错误
//Person(p3);
//隐士转换法
Person p4 = 10; //相当于写了 Person p4 = Person(10); 有参构造
Person p5 = p4; //拷贝构造
拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 以值传递方式给函数参数传值
- 以值方式返回局部对象
构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 1.默认构造函数(无参,函数体为空)
- 2.默认析构函数(无参,函数体为空)
- 3.默认拷贝函数,对属性进行值拷贝
构造函数调用规则如下
- 如果用户定义有参构造函数,C++不在提供默认无参构造函数,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
深拷贝与浅拷贝
- 浅拷贝:调用系统提供的拷贝构造函数对属性进行简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
- 总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
浅拷贝问题:
- 问题:属性在堆区申请空间,然后浅拷贝只是对地址简单拷贝操作(即两个不同类中的同一个属性指向同一个堆区内存空间),此时,如果在析构函数中释放该空间会出现重复释放的操作,系统会报错。
- 解决办法:自己写拷贝构造函数深拷贝,让类中的指针属性重新在堆区开辟内存空间存放相同内容,这样在析构函数中释放内存便不会报错
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数(可以有参数):属性1(值1(可以是变量)),属性2(值2)....{}
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
例如:
class A{};
class B
{
A a;
};
//B类中有对象A作为成员,A为对象成员
class Person
{
public:
//相当于隐式转换 Phone m_Phone = p_name ; 隐式转换
Person(string name , string p_name):m_Name(name) , m_Phone(p_name)
{
}
//人类
string m_Name;
//手机类
Phone m_Phone;
};
那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
当其他类对象作为本类成员,构造时候先构造类对象,再构造自身(先凑齐零件,才能构造自身),析构的顺序与构造相反
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量:
- 所有对象共享同一份数据(同一内存)
- 在编译阶段分配内存
- 类内声明,类外初始化
int Person::m_A = 100; //类外初始化
//静态成员变量有两种访问方式
//1、通过对象进行访问
p.m_A;
//2、通过类名进行访问
Person::m_A;
/*静态成员变量和静态成员函数都有访问权限,即访问权限设置为private类外不可以访问*/
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(因为非静态成员变量内存区域不是同一块,不知道具体访问哪个对象的成员变量)
C++对象模型和this指针
成员变量和成员函数分开存储
只有非静态成员变量才属于类对象上,静态成员变量,非静态成员函数,静态成员函数都不属于类对象上(即分配的内存空间不在类中)
空的类对象占用一个字节内存空间
this指针的用途
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的本质是 指针常量 指针的指向是不可以修改的
this指针的用途:
- 当形参和成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
//使用方法,区别成员变量和形参
this->age;//表示使用的是成员变量
//返回对象本身
Person& PersonAddAge(Person &p)//注意返回对象本身要用引用
{
this->age += p.age;
return *this;
}
Person p1(10);
Person p2(10);
//链式编程思想
p1.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2);
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
示例:
class Person
{
public:
void showClassName()
{
cout << "this is Person class" << endl;
}
void showClassAge()
{
cout << "age = " << m_age << endl;
}
int m_age;
};
Person *P = NULL;
p->showClassName();//该代码能正常运行
p->showClassAge();//报错,不能运行,因为m_age 相当于this->m_age,而this为NULL
//解决方法如下
void showClassAge()
{
if(this == NULL)
return ;
cout << "age = " << this->m_age << endl;
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数内不可以修改成员属性(相当于const Person * const this)即指针指向和指向值均不可修改
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象(const Person p)
- 常对象只能调用常函数(或mutable关键字修饰的成员变量),不可以调用普通成员函数,因为普通成员函数可以修改属性
友元
友元的目的就是让一个函数或者类访问另一个类中私有成员
友元关键字 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
//全局函数做友元
class Building
{
friend void geta();//需要在类中声明,类外全局函数才能使用类中私有变量
private:
int a;
};
void geta()
{
Building building;
building.a;
}
//类做友元
class Building;
class GoodGay
{
frienf class Building;
};
//成员函数做友元
class Building
{
void text();
};
class GoodGay
{
friend void Building::text();
};
运算符重载
加号运算符
作用:实现两个自定义数据类型相加的运算
//成员函数重载+号
Person operator+(Person &p)
{
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
//调用本质
Person p3 = p1.operator+(p2);//可以简化 Person p3 = p1 + p2;
//全局函数重载加号
Person operator+(Person &p1,Person & p2);
//调用
Person p4 = operator+(p1,p2); //简化 Person p4 = p1 + p2;
//运算符重载,也可以发生函数重载
//例如
Person operator+(Person &p1,int num);
Person p5 = p1 + 10;
总结1:对于内置的数据类型的表达式的运算符是不可能改变的(1 + 1 不可能=0)
总结2:不要滥用运算符重载(即operator+里只能做加法操作,不要是减法或除法)
左移运算符重载
作用:可以输出自定义数据类型
//只能利用全局函数重载左移运算符,注意函数要在类中声明友元,可以访问类中私有成员变量
//operator<<(cout , p) 简化 cout << p
//cout要用引用,因为全局只有一个,返回cout,因为链式编程,后面可以追加其他
ostream & operator<<(ostream & cout ,Person &p1)
{
cout << "m_A = " << p1.m_A << endl;
cout << "m_B = " << p1.m_B << endl;
return cout;
}
Person p;
cout << p <<endl;
递增运算符重载
//下面两个函数都是在类内定义
//前置递减,必须返回引用
Person & operator--()
{
m_A++;
return *this;
}
//后置递减,加个参数int是为了区分前置递增和后置递增,返回值,不能返回局部变量的引用
Person operator--(int)
{
Person temp = *this;
m_A++;
return temp;
}
总结:前置递增返回引用,后置递增返回值
赋值运算符重载
C++编译器至少给一个类添加4个函数,类中还提供了赋值运算符重载函数,operator=,对属性进行值拷贝
如果类中有属性指向堆区,做简单赋值操作时也会出现深浅拷贝问题
示例:
//在类中重载赋值运算符
Person& operator=(Person &p)
{
//编译器提供的是浅拷贝
//m_Age = p.m_Age;
//先判断是否有属性在堆区,有则先释放干净,然后再深拷贝
if(m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//深拷贝
m_Age = new int(*p.m_Age);
return *this;
}
Person p1,p2,p3;
//本质 p1.operator=(p2).operator=(p3);
p1 = p2 = p3;
//如果不重载赋值运算符,在堆区数据赋值然后在析构函数释放内存时,会重复释放同一内存造成报错
关系运算符重载
//重载==号 ,!=号也类似
bool operator==(Person &p)
{
if(this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return ture;
}
return false;
}
if(p1 == p2)
函数调用运算符重载
匿名对象运行完就释放
- 函数调用运算符()也可以重载
- 由于重载后使用方式非常像函数调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
//该函数在类内
int operator()(int num1 , int num2)
{
return num1 + num2;
}
//调用
Person p;
int ref = p(100,100);
cout << ref << endl;
//匿名函数对象也能调用,匿名对象执行完内存就会被释放
cout << Person()(100,100) << endl;
继承
总结:
- 继承的好处:可以减少重复代码
- 语法:class A :public B;
- A类称为子类或派生类
- B类称为父类或基类
派生类中的成员,包含两部分:
- 一类是从基类继承过来的,一类是自己增加的成员
- 从基类继承过过来的表现其共性,而新增的成员体现其个性
继承方式
继承方式:公共继承、保护继承、私有继承
继承语法:class 子类 : 继承方式 父类
继承中的对象模型
结论:父类中的所有非静态成员属性都会被子类继承下去,私有成员也会(即子类大小 = 父类 + 自身成员变量),只是由编译器给隐藏了访问不到
构造和析构顺序
总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
并且 子类对象继承父类,也会创建父类对象
继承同名成员处理方式
总结:
1.子类对象可以直接访问到子类中同名成员(s.m_Age直接访问)
2.子类对象加作用域可以访问到父类同名成员(s.Base::m_Age访问父类成员)
3.当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数(s.Base::func((100);)
静态同名静态成员处理方式
总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问方式(通过对象和通过类名)
//通过对象访问
s.func()
s.Base::func()
//通过类名访问
son::func()
son::Base::func()
多继承语法
C++允许一个类继承多个类
语法:class 子类 : 继承方式 父类 ,继承方式 父类2.....
多继承可以引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议使用多继承
总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域
菱形继承问题以及解决方法
菱形继承概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被称为菱形继承,或者钻石继承
菱形继承问题:羊继承了动物数据,坨也继承了动物数据,当羊驼使用数据时,就会产生二义性。羊驼继承了来自动物的两份数据,但其实我们只需要一份即可
解决办法:利用虚继承,解决菱形继承的问题,继承之前,加上关键字virtual变为虚继承,被继承的父类称为虚基类
//虚基类
class Animal;
//虚继承
class Sheep:virtual public Animal{};
class Tuo:virtual public Animal{};
//虚继承后,两份数据其实只有一份,共享同一个内存空间
底层本质:虚继承后,羊和坨只是继承了一个指针,各自指针指向自己的虚基类表,自身地址加上虚基类表中的偏移地址就能找到那一份共用的内存空间
多态
多态分为两类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal
{
//虚函数,加上virtual 关键字让函数地址晚绑定,即运行时才确定函数地址
virtual void speak()
{
}
}
//动态多态,希望传入什么对象,就调用什么对象的函数
void doSpeak(Animal & animal)//Animal & animal = cat
{
animal.speak();
}
Cat cat;
doSpeak(cat);
多态满足条件:
1.有继承关系
2.子类重写父类中的虚函数
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态使用:
父类指针或引用指向子类对象
多态底层原理:当父类写有虚函数时,此时父类中会保存有一个指向虚函数表的指针,这个虚函数表保存着父类虚函数入口地址,当子类继承父类时,会继承这个指向虚函数表的指针,如果子类中没有重写虚函数,则该虚函数表保存的仍然是父类的虚函数入口地址,当子类中重写父类中的虚函数,则该虚函数表中的虚函数入口地址被子类中的虚函数地址覆盖,也就是说此时存放的是子类中的虚函数入口地址(理解应该是函数调用时候动态改变虚函数表中虚函数入口地址)
多态优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期扩展以及维护
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
-
可以解决父类指针释放子类对象
-
都需要有具体的函数实现
虚析构和纯虚析构区别:
-
如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}(具体实现)
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
文件操作
文件类型分为两种:
-
文本文件 - 文件以文本的ASCII码形式存储在计算机中
-
二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
-
ofstream:写操作
-
ifstream: 读操作
-
fstream : 读写操作
文本文件
C++提高编程
模板
C++提供两种模板机制:函数模板和类模板
函数模板
函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表
语法:
- template<typename T>
- 函数声明或定义
解释:
- template ----声明创建模板
- typename----表明其后面的符号是一种数据类型,可以用class代替
- T -------通用的数据类型,名称可以替换,通常为大写字母
总结:
- 函数模板利用关键字template
- 使用函数模板有两种方式:自动类型推倒(myswap(a,b);)、显示指定类型(myswap<int>(a,b);)
- 模板的目的是为了提高复用性,将类型参数化
注意事项:
- 自动类型推导,必须推导出一致的数据类型T,才可以使用(例如:都是int)
- 模板必须要确定出T的数据类型才可以使用(使用时,必须要有确定数据类型func<int>())
普通函数与函数模板区别:
-
普通函数调用时可以发生自动类型转换(隐式类型转换)
-
函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换(不能确定具体是哪个变量类型)
-
如果利用显示指定类型的方式,可以发生隐式类型转换(手动指定数据类型)
总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T
普通函数与函数模板的调用规则
调用规则如下:
-
如果函数模板和普通函数都可以实现,优先调用普通函数(myPrint(a, b); //调用普通函数)
-
可以通过空模板参数列表来强制调用函数模板(myPrint<>(a, b); //调用函数模板)
-
函数模板也可以发生重载
-
如果函数模板可以产生更好的匹配,优先调用函数模板
总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
模板的局限性
局限性:
-
模板的通用性并不是万能的
例如:
template<class T>
void f(T a, T b)
{
a = b;
}
在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了
再例如:
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}
在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行
因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板
//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
if ( p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
{
return true;
}
else
{
return false;
}
}
类模板
类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板
//类模板
template<class NameType, class AgeType>
class Person
// 调用,指定NameType 为string类型,AgeType 为 int类型
Person<string, int>P1("孙悟空", 999);
类模板与函数模板区别主要有两点:
1.类模板没有自动类型推导的使用方式
// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
Person <string ,int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
2.类模板在模板参数列表中可以有默认参数
template<class NameType, class AgeType = int>
Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
类模板中成员函数和普通类中成员函数创建时机是有区别的:
-
普通类中的成员函数一开始就可以创建
-
类模板中的成员函数在调用时才创建
类模板对象做函数参数
一共有三种传入方式:
指定传入的类型 --- 直接显示对象的数据类型(常用,一般使用这种)
//1、指定传入的类型
void printPerson1(Person<string, int> &p)
{
p.showPerson();
}
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}
参数模板化 --- 将对象中的参数变为模板进行传递
整个类模板化 --- 将这个对象类型 模板化进行传递
类模板与继承
当类模板碰到继承时,需要注意一下几点:
-
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
-
如果不指定,编译器无法给子类分配内存
-
如果想灵活指定出父类中T的类型,子类也需变为类模板
//class Son:public Base//错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> //必须指定一个类型
{
};
//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2>
{
public:
Son2()
{
cout << typeid(T1).name() << endl;//int
cout << typeid(T2).name() << endl;//char
}
};
void test02()
{
Son2<int, char> child1;
}
类模板成员函数类外实现
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}
//总结:类模板中成员函数类外实现时,需要加上模板参数列表
类模板分文件编写
问题:
-
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决:
-
解决方式1:直接包含.cpp源文件
-
解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制(建议使用这种)
类模板与友元
全局函数类内实现 - 直接在类内声明友元即可(建议用)
全局函数类外实现 - 需要提前让编译器知道全局函数的存在(比较麻烦,一般采用类内实现)
//2、全局函数配合友元 类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template<class T1, class T2> class Person;
template<class T1, class T2>
void printPerson2(Person<T1, T2> & p)
{
cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
template<class T1, class T2>
class Person
{
//1、全局函数配合友元 类内实现
friend void printPerson(Person<T1, T2> & p)
{
cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
//全局函数配合友元 类外实现
friend void printPerson2<>(Person<T1, T2> & p);
}
STL
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
string容器
vector容器
-
vector数据结构和数组非常相似,也称为单端数组
vector与普通数组区别:不同之处在于数组是静态空间,而vector可以动态扩展
动态扩展:并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间
迭代器实际是一个地址
deque容器
3.3.1 deque容器基本概念:双端数组,可以对头端进行插入删除操作
deque与vector区别:
-
vector对于头部的插入删除效率低,数据量越大,效率越低
-
deque相对而言,对头部的插入删除速度回比vector快
-
vector访问元素时的速度会比deque快,这和两者内部实现有关
deque内部工作原理:
deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据,相当于一个一维数组被均分成若干段,每段的起始地址都保存在中控器中,该一维数组可以向两端动态扩展若干段,每段起始地址也是在中控器中保存
deque容器没有容量的概念
stack容器
栈不允许有遍历行为,栈是先进后出。
list容器
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。
总结:STL中**List和vector是两个最常被使用的容器,各有优缺点
list不支持下标形式访问数据,原因是list本质是链表,不是连续线性空间存储数据(可以it++)
list排序不可以运用系统标准算法,因为链表不支持随机访问迭代器,只能用内部提供的算法,排序可以指定排序规则
set/multies容器
-
所有元素都会在插入时自动被排序
set和multiset区别:
-
set不允许容器中有重复的元素
-
multiset允许容器中有重复的元素
set容器不允许重新指定容器大小,因为重新指定大小会有0重复元素出现
map容器
函数对象(仿函数)
本质:函数对象(仿函数)是一个类,不是一个函数
特点:
-
函数对象在使用时,可以像普通函数那样调用, 可以有参数,可以有返回值
-
函数对象超出普通函数的概念,函数对象可以有自己的状态
-
函数对象可以作为参数传递
概念:返回bool类型的仿函数称为谓词