1 内存分区模型
C++程序在执行时,将内存大致分为四个区域;
- 代码区:存放函数体的二进制代码,操作由系统管理
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值(形参),局部变量等
- 堆区:由程序员分配和释放,若程序员不手动释放,系统在程序结束时自动回收
1.1 程序运行前
代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中存在一份
- 代码区是只读的,使其只读是为了防止程序意外修改其指令
全局区:
- 全局变量、静态变量、常量{字符串常量,其他全局常量(const局部常量不存放在全局区)}存在在此区域
- 该区域数据在程序结束后由操作系统释放
1.2 程序运行后
栈区:
- 不要返回局部变量的地址,因为系统会自动回收此地址
堆区:
- 在C++中主要利用new,malloc在堆区开辟内存
- 释放内存空间一般使用delete,free对应上方2个申请内存空间的方法
2 引用
作用:
- 给变量取别名,别名指向原名的地址
- 别名修改其值,原名同样发生改变,因为其地址指向同一个
注意事项:
- 引用必须初始化
- 引用初始化之后就不能进行修改其指向的地址;内部实现是一个指针常量,指向的地址上的数据内容可以修改,但不能变更指向地址
用法:数据类型 &别名 = 原名
2.1 引用做函数参数
作用:
- 引用作为形参进行传递,形参发生改变,实参同样进行改变
- 可以简化指针的使用,因为引用指向同一个地址,修改同步
使用:
void swap(int &a,int &b){
...
//交换引用变量,实参随着形参的变化而变化
}
2.2 引用做函数返回值
作用:
- 引用可以作为函数返回值进行返回
- 返回的引用值可以作为左值
注意事项:
- 不要将局部变量作为引用返回,因为局部变量定义在栈区,会随着函数的结束而被回收
- 一旦局部变量被回收,则引用指向的是一个野地址,无意义
2.3 常量引用
作用:
- 用来修饰形参,防止形参误操作,改变实参的数据
- 修饰局部变量时,内部实现为const修饰的指针常量
用法:
int a = 10;const int &b = a;
//const int *const b = &a;
//上述等价
3 函数
3.1 函数默认参数
默认参数必须连续的定义在形参列表末尾,且不能被非默认参数隔断
void func(int a,int b=10,int c=20){
...
}
如果函数声明的形参拥有默认参数,则在函数实现部分不能在出现默认参数定义
结论:函数声明和函数实现只能有一个存在默认参数
void func(int a,int b=10);
void func(int a,int b){
...
}
3.2 占位参数
在形参列表中允许占位参数,但在实参传递时,必须填充;
占位参数也可以拥有默认参数
void func(int = 20,double){
...
}
int main(){
func(10,0.5f);
return 0;
}
3.3 函数重载
3.3.1 函数重载概述
重载:
- 函数名可以重复,但形参列表个数、类型、顺序需不一致
- 返回类型不相同与否,不能作为函数重载的条件
- 在同一作用域
void func004(int a){
}
void func004(double a){
}
void func004(int a,double b){
}
void func004(double a,int b){
}
3.3.2 函数重载的注意事项
引用作为函数重载的条件:
- 以下调用会引用func005(int &a)
- 因为a是一个局部变量,它的地址声明在栈区
int a = 10;
func005(a);
void func005(int &a){
cout << "int &a" << endl;
}
- 以下调用会引用func005(const int &a)
- 因为b和10是一个局部常量,只允许只读操作,与上述重载函数不存在二义性
func005(10);
const int b = 10;
func005(b);
void func005(const int &a){
cout << "const int &a" << endl;
}
当函数重载使用默认参数时:
- 当函数重载出现二义性时,会出现错误,如下所示
- 例如调用func006(10);时,编译器不知道该调用哪一个函数,故报错
void func006(int a){
...
}
void func006(int a,int b = 10){
...
}
4 类和对象
面向对象三大特性:
- 封装
- 继承
- 多态
4.1 封装
4.1.1 意义
- 将属性和行为作为一个整体
- 将属性和行为加以权限控制
class Circle{
//私有访问权限
private:
//私有成员变量半径r
int r;//半径
//公共访问权限
public:
//构造函数,给私有成员半径r赋初始值
Circle(int r):r(r){}
//公共权限函数,计算周长
double calculate(){
return 2 * PI * r;
}
};
int main(int argc, const char * argv[]) {
Circle l_circle(2);
double zc = l_circle.calculate();
cout << "周长=" << zc << endl;
return 0;
}
4.1.2 访问权限
- Private(私有权限):成员 类内可以访问 类外不可以访问
- Protected(保护权限):成员 类内可以访问 类外不可以访问
- Public(公共权限):成员 类内可以访问 类外可以访问
注意事项:Private和Protected区别在于,当继承时
- 如果继承权限为Protected,子类可以访问父类的Protected和Publich成员,但不能访问Private成员
- 如果继承权限为Private,子类不能访问父类定义的Private、Protected和Publich成员
4.1.3 struct和class区别
struct和class区别在于默认访问权限不同
- struct 默认访问权限为公共
- class 默认访问权限为私有
4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
构造函数意义:
- 主要作用于创建对象时,给类成员赋初始化值
- 构造函数由编译器自动调用
- 每一个类都会存在一个public访问权限的空参默认构造函数
析构函数意义:
- 主要用于在对象销毁前,系统会自动调用,将此对象内存地址回收
构造函数语法:类名(){}
- 构造函数没有返回值,且不能写void
- 构造函数名称与类名一致
- 构造函数可以含有形参,默认无参,可以发生重载
- 构造函数由编译器自动调用,且调用一次
析构函数语法:~类名(){}
- 析构函数没有返回值,且不能写void
- 析构函数与类名一致,且在前方将~符号
- 析构函数不能含有参数,不能发生重载
- 析构函数由编译器自动在对象销毁前调用,且调用一次
4.2.2 构造函数的分类及调用
分类方式:
- 按参数分:有参构造和无参构造
- 按类型分:普通构造和拷贝构造
拷贝构造函数写法:const 类名 &对象名
Student(const Student &stu){
name = stu.name;
age = stu.age;
score = stu.score;
}
调用方式:
- 括号法
//默认构造函数,使用默认构造函数时,不要添加();
//因为编译器会认定为一个函数声明
Student stu;
Student lisi("李四",23,90.0); //有参构造函数
Student wangwu(lisi); //拷贝构造函数
- 显示法
Student niuer = Student("牛二");//有参构造函数
Student zhaoer = Student(niuer);//拷贝构造函数
//匿名对象
//执行完此行之后,内存空间被系统回收
Student("匿名");
//不要利用拷贝构造函数初始化匿名对象
//编译器会认定Student(zhaoer) === Student zhaoer;
//则认定为重新定义一个无参构造类对象,但此名称已被使用,所以产生重定向
Student(zhaoer);//语法错误
- 隐式转换法
Student zhaosan = 10;//等价于 Student zhaosan = Student(10);
Student wangqi = zhaosan;//拷贝构造
4.2.3 拷贝构造函数
使用构造函数的调用时机:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
调用Student lisi;语句时,通过无参默认构造函数建立一个对象
当调用execute(lisi);语句时,则在形参列表通过拷贝构造函数建立一个对象形参
void execute(Student stu){
...
}
...
Student lisi;
execute(lisi);
- 以返回值的方式返回局部对象
在当下测试时,返回的是局部对象的地址,也就是说,两者对象指向同一个地址
并不会再去调用拷贝构造函数,然后将建立的地址进行返回
结论:编译器默认优化,故没有调用拷贝函数,关闭默认优化则会显现
Student execute(){
...
return Student("name");
}
...
Student stu = execute();
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加三个函数:
- 默认无参构造函数
- 默认析构函数
- 默认拷贝构造函数,对属性进行值拷贝
规则:
- 如果程序员定义了有参构造函数,则C++不再提供默认构造函数,但会提供默认拷贝构造函数
- 如果程序员定义了拷贝构造函数,则C++不再提供其他构造函数(默认构造函数和默认拷贝构造函数)
4.2.5 深拷贝和浅拷贝
- 浅拷贝:值拷贝
浅拷贝带来的问题:堆区的内存被重复释放
当建立一个对象,在构造函数中给成员id在堆区开辟一个内存空间,并对该对象进行初始化
然后通过编译器默认拷贝函数进行浅拷贝
由于id为指针,两个对象指向的都是同一堆区地址,于是当一个对象被回收之后,调用析构函数
清除id的内存引用;随机另外一个对象也被回收,同样调用析构函数;会出现堆区内存被重复释放;
此处的空判断,是对当前对象的成员id的地址进行判断,堆区内存引用虽被释放,但内存地址仍旧存在;
string name;
int *id;
...
Student(string name,int id){
this->name = name;
this->id = new int(id);
}
~Student(){
if(id != NULL){
delete id;
id = NULL;
}
cout << "析构函数-释放资源" << endl;
}
...
//浅拷贝
Student zhangsan("张三",110);
Student lisi(zhangsan);
- 深拷贝:在堆区重新申请空间,进行拷贝操作
解决办法:不使用系统提供的默认拷贝构造函数,通过自定义一个拷贝构造函数
在堆区内重新开辟一段内存地址存放id,这样两个对象的成员id所指向的内存地址不一样,就不会产生上述问题。
Student(const Student &stu){
name = stu.name;
id = new int(*stu.id);
}
总结
- 浅拷贝:会创建一个新的对象,将旧对象的数据内容拷贝到新对象之中;如果是基本类型,则通过值拷贝;如果是引用类型,则将旧对象的成员地址拷贝给新对象;也就是说新旧对象不一样,但引用类型成员的地址指向同一个;
- 深拷贝:对于基本数据类型,深拷贝同样采用值传递;对于引用类型,则重新在堆区申请内存空间,并使用旧对象的数据对新对象进行初始化。
4.2.6 初始化列表
语法:构造函数():属性1(值1),属性2(值2){}语法:构造函数(形参1 标识符1,形参2 标识符2):属性1(标识符),属性2(标识符2){}
4.2.7 类对象作为类成员
C++类中的成员变量可以是另一个类的对象,称为对象成员;
当其他类对象作为本类成员,先构造其他类对象,然后在构造本身;
本类先调用析构函数,然后在由其他类成员调用其析构函数
4.2.8 静态成员
静态成员即在成员变量和成员函数前加static
静态成员访问方式:
- 通过类名::静态成员(静态变量与静态函数一致)
- 通过初始化类对象进行访问
静态成员变量:
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 静态成员函数也拥有访问权限
静态函数只能调用静态变量:因为静态函数在内存中只有一份,而非静态成员变量,可以被多个类对象创建引用,编译器无法识别静态函数中的非静态变量是哪一个类对象的成员,故无法引用
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C++中,类的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上;
在下列代码中,除了非静态变量age属于类对象上,其余都不属于;则Student类大小为4字节(32位),静态成员变量、静态成员函数、非静态成员函数都不熟类对象上的内存空间
class Student{
int age;
static int id;
void func1(){}
static void func2(){}
};
Student stu;
空对象占用内存空间为1字节;是为了区分空对象在内存上的位置;
4.3.2 this指针
this指针指向被调用的成员函数所属的对象
用途:
- 当形参和成员变量同名时,可以用this指针区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
4.3.3 空指针访问成员函数
空指针允许调用成员函数,但成员函数内不允许调用成员变量;
因为成员变量没有实际的对象,没有实际地址;
void test(){
Student *stu = NULL;
stu->toString();
}
4.3.4 const修饰成员函数
常函数:
- 成员函数后加const后,称为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,允许在常函数中修护成员属性
class Dog{
private:
string name;
mutable int age;
public:
///常函数:不允许修改成员属性
///this指针的本质是指针常量,不允许修改指向地址,但可以修改其值
///Dog * const this;
///当成员函数加上const之后,this指针变为了 const Dog* const this;即地址也不能修改,值也不能修改
void eat() const{
//this->name = "aaa"; //error
}
///常函数
///当想要在常函数中修改成员属性,即需要在成员属性前加mutable
void bark() const{
///age 被mutable修饰,故可以在常函数中修改
this->age = 7;
}
};
常对象:
- 声明对象前加const,称该对象为常对象
- 常对象依旧不能修改成员属性,除非修改加有mutable修饰的成员变量即可以修改
- 常对象只能调用常函数
常对象之所以不能调用普通函数,是因为普通函数可以修改成员属性;如果常对象可以调用普通函数,则与常对象不能修改非mutable成员属性相违背
4.4 友元
友元:让有些私有属性,可以被类外一些特殊的函数和类进行访问
友元三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
访问类中私有成员变量和私有成员函数
只需将访问函数放到类中进行声明并前方加friend关键字进行修饰即可
class Attribute{
//声明全局友元函数
friend void visitor(Attribute *attribute);
private:
string privateAttr;
public:
string publicAttr;
public:
Attribute(){
privateAttr = "私有属性";
publicAttr = "公共属性";
}
private:
void toString(){
cout << "正在访问私有函数" << endl;
}
};
///访问类中私有成员变量和私有成员函数
///只需将访问函数放到类中进行声明并前方加friend关键字进行修饰即可
void visitor(Attribute *attribute){
cout << "访问成员:" << attribute->privateAttr << endl;
attribute->toString();
}
4.4.2 类做友元
类A中包含一个类B成员对象,若A对象想要访问B的私有属性,则需要在类B中添加友元类声明
class B{friend class A;
...
};
class Visitor{
public:
Building *attr;
Visitor();
void behavior();
};
class Building{
//声明Visitor类为本类的友元
friend class Visitor;
private:
string style;
public:
string name;
public:
Building();
};
///类外实现类内构造函数
Building::Building(){
name = "圆明园";
style = "中式";
}
///类外实现类内构造函数
Visitor::Visitor(){
attr = new Building;
}
///类外实现类内公共成员函数
///在类外实现成员函数,跟类内一样,对成员函数、成员变量拥有同样的访问权限
void Visitor::behavior(){
//访问成员对象公共属性
cout << "访问建筑:" << attr->name << endl;
//访问成员对象私有属性
cout << "建筑样式:" << attr->style << endl;
}
4.4.3 成员函数做友元
不声明类做为友元类,声明类中成员函数作为友元函数,让成员函数可以访问成员对象的私有属性
class Consumer{
public:
Phone *phone;
Consumer();
void svip();
void phoneType();
};
class Phone{
//声明另一个类的函数为友元函数,则此函数可以访问本类中的私有属性
friend void Consumer::svip();
private:
string model;
public:
string name;
Phone();
};
Phone::Phone(){
model = "麒麟9000";
name = "华为mate";
}
Consumer::Consumer(){
phone = new Phone;
}
//友元函数,已经在对象类中声明此函数为友元函数,运行访问成员对象的私有属性
void Consumer::svip(){
cout << "名称:" << phone->name;
cout << " ";
cout << "型号:" << phone->model;
}
//非友元函数,无法访问成员对象的私有属性
void Consumer::phoneType(){
cout << "名称:" << phone->name << endl;
//cout << " ";
//cout << "型号:" << phone->model;
}
4.5 运算符重载
对运算符重新进行定义,赋予一种新的功能
4.5.1 加号运算符重载
通过成员函数重载运算符+
Person operator +(const Person &a){
Person temp;
temp.age = this->age + a.age;
temp.name = this->name + a.name;
return temp;
}
//运算符重载函数可以发生重载
Person operator +(int age){
return Person("重载运算符",this->age + age);
}
通过全局函数重载运算符+
Person operator + (const Person &a,const Person &b){
Person temp;
temp.name = a.name + b.name;
temp.age = a.age + b.age;
return temp;
}
调用
void test01(){
Person zhangsan("张三",22);
Person lisi("李四",24);
///下面两种方式都可以调用函数重载运算符+
//成员重载函数本质:Person wangwu = zhangsan.operator+(lisi);
Person wangwu = zhangsan + lisi;
//全局重载函数本质:Person zhaoer = zhaoer.operator+(zhangsan,lisi);
Person zhaoer = zhangsan + lisi;
//运算符重载函数可以发生重载
Person liqi = zhangsan + 20;
wangwu.toString();
}
4.5.2 左移运算符重载
左移运算符重载一般不放到成员函数内,一般放到全局函数;
因为成员函数内需要传入2个对象才能完成需求,但又不符合需要;
例如:lisi.operator <<(wangwu),但不符合 cout << wangwu的需求
cout的类型为ostream,通过返回ostream类型,形成链式
ostream& operator <<(ostream &cout,const Person ¤t){
return cout << "姓名:" << current.name << " " << "年龄:" << current.age;
}
...
Person lisi("李四",24);
cout << lisi << endl;
4.5.3 递增运算符重载
class MyInteger{
friend MyInteger& operator ++(MyInteger &myint);
friend ostream& operator <<(ostream &cout,const MyInteger &myint);
friend MyInteger operator ++(MyInteger &myint,int);
private:
int count;
public:
MyInteger(){
count = 0;
}
};
//重载前置++运算符
MyInteger& operator ++(MyInteger &myint){
myint.count++;
return myint;
}
///重载后置++运算符
///operator++ (int) int代表占位参数,用来区分前置和后置++
MyInteger operator ++(MyInteger &myint,int){
MyInteger temp = myint;
myint.count++;
return temp;
}
//重载<<运算符
ostream& operator <<(ostream &cout,const MyInteger &myint){
return cout << myint.count;
}
void test02(){
MyInteger myint;
//前置++
cout << ++(++myint) << endl;
cout << myint << endl;
//后置++
cout << myint++ << endl;
cout << myint << endl;
}
4.5.4 赋值运算符重载
如果类中有属性指向堆区,赋值操作会引起深浅拷贝问题;
在进行重载=运算符时,应判断类中是否存在堆区属性,如果存在,则应该清空,然后进行深拷贝赋值
class Calculate{
friend void test03();
private:
int *count;
public:
Calculate(int count){
//内存分配到堆区
this->count = new int(count);
}
~Calculate(){
if(count != NULL){
delete count;
count = NULL;
}
}
///重载=运算符
Calculate& operator=(const Calculate ¶m){
if(count != NULL){
delete count;
count = NULL;
}
this->count = new int(*param.count);
return *this;
}
};
void test03(){
Calculate c1(10);
Calculate c2(20);
Calculate c3(30);
c3 = c2 = c1;
cout << "value = " << *c3.count << endl;
}
4.5.5 关系运算符重载
class Compare{
int num;
public:
Compare(int num){
this->num = num;
}
//重载>运算符
bool operator >(const Compare &a){
if(this->num > a.num){
return true;
}
return false;
}
//重载<运算符
bool operator <(const Compare &a){
if(this->num < a.num){
return true;
}
return false;
}
//重载==运算符
bool operator ==(const Compare &a){
if(this->num == a.num){
return true;
}
return false;
}
};
void test04(){
Compare a(10);
Compare b(20);
cout << "a>b " << (a>b) << endl;
cout << "a<b " << (a<b) << endl;
cout << "a==b " << (a==b) << endl;
}
4.5.6 函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class Operate{
public:
void operator() (string content){
cout << content << endl;
}
void operator() (int content){
cout << content << endl;
}
int operator() (int a,int b){
return a+b;
}
};
void test05(){
Operate operat;
operat("hello world!");
int result = operat(10,20);
operat(result);
}
4.6 继承
基类又称父类
派生类又称子类
子类通过继承父类,可以获取父类表现为共性的公共属性,也保留自身特性的成员属性
4.6.1 继承方式
继承方式:
- 公共继承
继承方式为公共属性,则父类的公共成员属性与保护成员属性在子类保持不变,父类私有成员不允许访问
- 保护继承
继承方式为保护属性,则父类的公共成员属性在子类变为保护成员属性,父类保护成员属性在子类保持不变,父类私有成员不允许访问
- 私有继承
继承方式为私有属性,则父类的公共成员属性与保护成员属性在子类变为私有成员属性,父类私有成员不允许访问
class Animal{
private:
string sounds;
protected:
string moving;
public:
string name;
};
//继承权限为公共权限
class Dog: public Animal{
public:
void func(){
name = "狗";
moving = "爬";
//sounds 不允许访问
}
};
//继承权限为保护权限
class Bird: protected Animal{
public:
void func(){
name = "鸟";
moving = "飞";
//sounds 不允许访问
}
};
//继承权限为私有权限
class Fash: private Animal{
public:
void func(){
name = "鱼";
moving = "游";
//sounds 不允许访问
}
};
void test01(){
Dog dog;
dog.name = "狗";
//保护权限,不允许访问 dog.moving = "爬";
Bird bird;
//保护权限,不允许访问 bird.name = "鸟";
//保护权限,不允许访问 bird.moving = "飞";
Fash fash;
//私有权限,不允许访问 fash.name = "鱼";
//私有权限,不允许访问 fash.moving = "游";
}
4.6.2 继承中的对象模型
子类继承的父类,父类的所有成员属性都会被继承(私有成员也会被继承,只是被隐藏);
子类的成员属性包括父类所有成员属性和自身成员属性;
例如:父类有公共成员属性、保护成员属性、私有成员属性各一个整型成员(设int 占四个字节)
子类有一个整型成员变量,则子类占内存空间为16字节
4.6.3 继承中的构造和析构
子类继承父类,先调用父类的构造函数,然后调用子类构造函数
程序结束时,先析构子类对象,然后析构父类对象
4.6.4 继承同名成员
当子类和父类出现同名成员属性时
- 访问子类同名成员,可直接调用
- 访问父类同名成员,通过作用域符号::调用
- 如果子类出现了与父类同名成员函数,会屏蔽父类中所有同名成员函数
class Base{
public:
int age;
Base(){
age = 10;
cout << "Base construcat" << endl;
}
~Base(){
cout << "Base destroy" << endl;
}
void func(){
cout << "Base func" << endl;
}
};
class Sub:public Base{
public:
int age;
Sub(){
age = 20;
cout << "Sub construcat" << endl;
}
~Sub(){
cout << "Sub destroy" << endl;
}
void func(){
cout << "Sub func" << endl;
}
};
void test02(){
Sub sub;
cout << "sub age=" << sub.age << endl;
cout << "parent age=" << sub.Base::age << endl;
sub.func();
sub.Base::func();
}
4.6.5 继承中同名的静态成员
静态成员与非静态成员出现同名,处理方式一致
- 访问子类同名成员,直接访问计科
- 访问父类同名成员,需要加作用域
4.6.6 多继承
C++内允许一个子类继承多个父类
语法:class 子类名称:继承方式 parent1,继承方式 parent2,...{...
};
当父类中出现了同名的成员,则需要加作用域进行访问
4.6.7 菱形继承
菱形继承概念
两个派生类同时继承一个基类,又有某个类同时继承两个派生类
例如:有一个基类动物,羊和乌龟同时继承动物类,又存在一个沸羊羊双面龟类继承羊和乌龟。
菱形继承存在的问题:
- 羊和乌龟同时继承动物的属性,当沸羊羊双面龟类使用其共有属性时,存在二义性
可通过作用域进行访问
- 沸羊羊双面龟继承了两份同样的公共属性
可通过虚继承进行解决,使用虚继承之后,就相当于这一个同名公共属性就只存在一份,子类共享这一个属性
//Animal称为虚基类
class Animal{
public:
int age;
};
///添加virtual 称为虚继承
class Sheep:virtual public Animal{
};
class Turtle:virtual public Animal{
};
class Dog:public Sheep,public Turtle{
};
void test03(){
Dog dog;
dog.Sheep::age = 10;
dog.Turtle::age = 20;
//使用虚继承之后,就相当于这一个公共属性age就只存在一份,羊和乌龟共享着=这一个属性
cout << "羊的年龄:" << dog.Sheep::age << endl;//输出20
cout << "乌龟的年龄:" << dog.Turtle::age << endl;//输出20
cout << "乌龟的年龄:" << dog.age << endl;//输出20
}
4.7 多态
4.7.1 多态的概念
多态类型:
- 静态多态:函数重载和运算符重载
- 动态多态:派生类和虚函数实现运行时多态
多态的区别:
- 静态多态函数地址早绑定;编译阶段确定函数地址
- 动态多态函数地址晚绑定;运行阶段确定函数地址
动态多态的满足条件:
- 有继承关系
- 子类重写父类成员函数
动态多态的使用:父类指针或引用执行子类对象
class Animal{
public:
///将此函数定义为虚函数
void virtual speak(){
cout << "动物在叫..." << endl;
}
};
class Cat: public Animal{
public:
void speak(){
cout << "喵喵喵..." << endl;
}
};
///即使传入的是子类对象,但依旧调用的是父类重名函数
///属性静态多态,地址在编译阶段确定函数地址
///如果需要执行子类同名函数,需要在运行阶段确定函数地址-动态多态,在执行函数加virtual,称为虚函数
void speaking(Animal &animal){
animal.speak();
}
void test01(){
///根据传入的对象作为最终的函数形参对象-虚函数
Cat cat;
speaking(cat);
Animal animal;
speaking(animal);
}
4.7.2 纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要使用的是子类重写内容,因此可以改为纯虚函数
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中包含了纯虚函数,此类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Person{
public:
///纯虚函数,拥有纯虚函数的类称为抽象类
///抽象类不允许被实例化
virtual void speaking() = 0;
};
class Student: public Person{
public:
///子类继承抽象类,必须重写纯虚函数;否则依旧是一个抽象类
///子类重写的纯虚函数,virtual可写可不写
void speaking(){
cout << "为中华之崛起而读书" << endl;
}
};
void test02(){
Person *zhangsan = new Student;
zhangsan->speaking();
Student lisi;
lisi.speaking();
}
4.7.4 虚析构和纯虚析构
在多态中,如果子类中有成员属性内存地址在堆区,那么父类指针在释放时无法调用子类的析构函数
解决方案:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象问题
- 都需要有具体的函数实现
虚析构和纯虚析构的异性:
- 如果为纯虚析构,则此类为抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
声明: virtual ~类名() = 0;实现: 类名::~类名(){}
虚析构或纯虚析构用来解决父类指针释放子类对象
如果子类中没有堆区数据,可以不写虚析构或纯虚析构
拥有纯虚析构函数的累属于抽象类
class Person{
public:
///纯虚函数,拥有纯虚函数的类称为抽象类
///抽象类不允许被实例化
virtual void speaking() = 0;
//虚析构,可以解决父类指针释放子类对象堆区成员属性不干净问题
// virtual ~Person(){
//
// }
///纯虚析构函数定义
virtual ~Person() = 0;
};
///纯虚析构函数实现
Person::~Person(){
cout << "父类纯析构" << endl;
}
class Student: public Person{
public:
string *name;
Student(string name){
this->name = new string(name);
}
///子类继承抽象类,必须重写纯虚函数;否则依旧是一个抽象类
///子类重写的纯虚函数,virtual可写可不写
void speaking(){
cout << *(this->name) << "为中华之崛起而读书" << endl;
}
~Student(){
if(name != NULL){
cout << *(this->name) << "对象被销毁" << endl;
delete name;
name = NULL;
}
}
};
void test02(){
///通过父类指针指向子类对象,当析构父类对象时,子类中的堆区对象并不会被销毁
///需要将父类析构函数改为虚析构或者纯虚析构
Person *zhangsan = new Student("张三");
zhangsan->speaking();
delete zhangsan;
}
父类中有纯虚函数,说明这个父类是一个抽象类,不能进行实例化。但是,可以通过指针的方式指向子类的实例化对象地址,因为子类继承了父类的纯虚函数并提供了具体实现,使得子类对象满足了父类的要求,可以进行实例化。通过指针方式使用子类对象,也就是通过父类的接口访问子类的实现,实现了多态性。
5 文件操作
C++中对文件操作需要包含头文件
文件类型分为:
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中
操作文件的三大类:
- ofstream:写操作
- ifstream:读操作
- frstream:读写操作
5.1 文本文件
5.1.1 写文件
写文件操作步骤:
- 包含头文件:#include
- 创建流对象:ofstream ofs;
- 打开文件:ofs.open("文件路径",打开方式)
- 写数据:ofs << "写入的内容";
- 关闭文件:ofs.close();
打开方式 | 备注 |
---|---|
ios::in | 为读文件而打开 |
ios::out | 为写文件而打开 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在则删除,在创建 |
ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用|操作符
例如:用二进制方式写文件ios::binary | ios::out
#include <fstream>
...
void writeFile(){
///1.创建文件流
ofstream ofs;
///2.打开文件test.txt,打开方式写文件
///如果不加绝对路径,则默认文件创建在与项目同级路径
ofs.open("/Users/FranzLiszt/Downloads/test.txt",ios::out);
///3.写内容
ofs << "姓名:张三" << endl;
ofs << "年龄:32" << endl;
ofs << "性别:不详" << endl;
///4.关闭文件流
ofs.close();
}
5.1.2 读文件
读文件操作步骤:
- 包含头文件:#include
- 创建流对象:ifstream ifs;
- 打开文件:打开文件并判断是否打开成功,ifs.open("文件路径",打开方式)
- 读数据:包含四种方式
- 关闭文件:ifs.close();
void readFile(){
///1.创建文件流
ifstream ifs;
///2.打开文件test.txt,打开方式读文件
ifs.open("/Users/FranzLiszt/Downloads/test.txt",ios::in);
///3.判断是否打开成功
if(!ifs.is_open()){
cout << "文件打开失败!" << endl;
ifs.close();
return;
}
///4.读数据
///4.1 第一种读取方式
char buff1[1024] = {0};
while (ifs >> buff1) {
cout << buff1 << endl;
}
///4.2 第二种读取方式
char buff2[1024] = {0};
while (ifs.getline(buff2, sizeof(buff2))) {
cout << buff2 << endl;
}
///4.3 第三种读取方式
string buff3;
while (getline(ifs, buff3)) {
cout << buff3 << endl;
}
///4.4 第四种方式
char ch;
while ((ch = ifs.get()) != EOF) {
cout << ch;
}
ifs.close();
}
5.2 二进制文件
对二进制方式进行操作要将打开方式指定为ios::binary
5.2.1 写二进制文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char* buffer,int len);
字符指针buffer指向内存中一段存储空间,len是读写的字节数
void writeBinary(){
//1.创建流对象
ofstream ofs;
//2.打开文件,打开方式为写|二进制文件
ofs.open("/Users/FranzLiszt/Downloads/testBinary.txt",ios::out|ios::binary);
char name[] = "张三";
int age = 32;
Person zhangsan(name,age);
///3.写入内容
ofs.write((const char*)&zhangsan, sizeof(zhangsan));
///4.关闭流文件
ofs.close();
}
5.2.2读二进制文件
void readBinary(){
//1.创建流对象
ifstream ifs;
//2.打开文件,打开方式为写|二进制文件
ifs.open("/Users/FranzLiszt/Downloads/testBinary.txt",ios::in|ios::binary);
//3.判断文件是否打开成功
if(!ifs.is_open()){
cout << "文件打开失败!" << endl;
ifs.close();
return;
}
///4.读取内容
Person zhangsan;
char buff[1024] = {0};
ifs.read((char *)&zhangsan, sizeof(Person));
cout << "姓名:" << zhangsan.name << endl;
cout << "年龄:" << zhangsan.age << endl;
///5.关闭流文件
ifs.close();
}
6 模版
6.1 模版的概念
模版就是建立通用的模具,提高复用性
C++另一种编程思想称为泛型编程,主要利用的技术就是模版
C++提供两种模版机制:函数模版和类模版
特点:
- 模版不可以直接使用,它只是一个框架
- 模版并不是万能的
6.2 函数模版
6.2.1 函数模版基本用法
函数模版的作用:
- 建立一个通用函数,其函数返回值类型和形参类型可以不具体指明,用一个虚拟类型代替
语法:template 函数定义或声明
注:
- template:声明创建模版
- typename:表明其后面的符号是一种数据类型,可以用class代替
- T:通用数据类型,名称可以自定义
//声明一个模版,告诉编译器后面的代码中的T不要报错,T是一个通用数据类型
template<typename T> void swap(T &a,T &b){
T temp = a;
a = b;
b = temp;
}
...
void test01(){
int a = 10;
int b = 20;
//1.自动类型推导
swapValue(a, b);
//2.显示指定类型
swapValue<int>(a, b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
}
6.2.2 函数模版注意事项
注意事项:
- 自动类型推导,必须推导的一致的数据类型T,才可以使用
- 模版必须要确定出T的数据类型,才可以使用
templatetemplate
上述两种写法,没有区别,效果一致
6.2.3 函数模版示例
示例描述:
- 利用函数模版封装一个排序的函数,可以对不同的数据类型数组进行排序
- 排序按降序,排序算法为选择排序
//声明一个模版,告诉编译器后面的代码中的T不要报错,T是一个通用数据类型
template<typename T> void swapValue(T &a,T &b){
T temp = a;
a = b;
b = temp;
}
/// Description 利用模版技术写一个选择排序升序排序算法
/// - Parameters:
/// - array: 数组元素首地址
/// - length: 数组内容长度
template<typename T>
void selectSort(T array[],int length){
int min;
for(int i=0;i<length;i++){
min = i;
for(int j=i+1;j<length;j++){
if(array[min] > array[j]){
min = j;
}
}
if(i != min){
swapValue(array[i], array[min]);
}
}
}
/// Description 利用模版技术写一个打印函数
/// - Parameters:
/// - array: 数组元素首地址
/// - length: 数组内容长度
template <typename T>
void printfArray(const T array[],int length){
for(int i=0;i<length;i++){
cout << array[i];
}
cout << endl;
}
void test02(){
int a[] = {2,5,7,8,1,0};
char b[] = "cabfdg";
int aLength = sizeof(a) / sizeof(int);
int bLength = sizeof(b) / sizeof(char);
selectSort(a,aLength);
selectSort(b,bLength);
printfArray(a,aLength);
printfArray(b,bLength);
}
6.2.4 普通函数与函数模版的区别
区别:
- 普通函数调用可以发生自动类型转换(隐式类型转换)
- 函数模版调用时,如果利用自带类型推导,不会发生隐式类型转换
- 如果利用显示指定类型方式,可以发生隐式类型转换
///普通函数允许隐式类型转换
int normalAdd(int a,int b){
return a+b;
}
void test03(){
int a = 10;
char b = 'b';
//字符'b'对应的ascii码为98,所有自动将char类型转为int类型
int result = normalAdd(a, b);
cout << "reuslt=" << result << endl;
}
template <typename T>
T templateAdd(T a, T b){
return a+b;
}
void test04(){
int a = 10;
char b = 'b';
//函数模版的自动推导类型无法实现隐式类型转换,也就是无法将char转为int,无法完成下列语句
//int result = templateAdd(a, b);
//cout << "reuslt=" << result << endl;
//函数模版的显示指定类型可以实现隐式类型转换,下列语句正常执行
int result = templateAdd<int>(a, b);
cout << "reuslt=" << result << endl;
}
6.2.5 普通函数与函数模版的调用规则
调用规则:
- 如果函数模版和普通函数都可以实现,优先调用普通函数
- 可以通过空模版参数列表来强制调用函数模版
- 函数模版也可以发生重载
- 如果函数模版可以产生更好的匹配,则优先调用函数模版
void myPrintf(int a,int b){
cout << "normal function" << endl;
}
template <typename T> void myPrintf(T a,T b){
cout << "template function" << endl;
}
///函数模版也可以发生重载
template <typename T> void myPrintf(T a,T b,T c){
cout << "overload template function" << endl;
}
void test05(){
int a = 10;
int b = 20;
int c = 30;
///如果函数模版和普通函数都可以实现,优先调用普通函数
///如果普通函数只有声明没有实现,模版函数存在,会出现编译错误,因为优先调用普通函数,但是普通函数没有实现,故错误
myPrintf(a, b);
///可以通过空模版参数列表来强制调用模版函数
myPrintf<>(a, b);
///函数模版也可以发生重载
myPrintf(a, b,c);
///如果函数模版可以产生更好的匹配,则优先调用函数模版
char ca = 'a';
char cb = 'b';
myPrintf(ca, cb);
}
6.2.6 模版的局限性
模版的通用性并不是万能的,某些特定数据类型,需要具体方式做特殊实现
///具体化模版
template<> bool compare(Student &a,Student &b){
if(a.name == b.name && a.age == b.age){
return true;
}
return false;
}
void test06(){
Student zhangsan("张三",32);
Student lisi("张三",32);
bool result = compare(zhangsan, lisi);
if(result){
cout << "same" << endl;
}else{
cout << "different" << endl;
}
}
6.3 类模版
6.2.1 类模版基本用法
类模版的作用:
- 建立一个通用函数,类中成员的数据类型可以不具体指明,用一个虚拟类型代替
语法:template 类
注:
- template:声明创建模版
- typename:表明其后面的符号是一种数据类型,可以用class代替
- T:通用数据类型,名称可以自定义
///类模版,对于通用的数据类型可以使用虚拟类型来代替
template <class T,class W>
class Animal{
public:
T name;
W age;
Animal(T name,W age){
this->name = name;
this->age = age;
}
void toString(){
cout << this->name << this->age << "岁了" << endl;
}
};
void test07(){
//模版参数列表
Animal<string, int> cat("喵喵",2);
cat.toString();
}
6.3.2 类模版和函数模版的区别
主要区别:
- 类模版没有自动类型推导方式,只有显示指定类型方式
- 类模版在模版参数列表可以有默认参数
///类模版参数列表允许有默认参数
template <class T,class W = int>
class Animal{
...
};
...
///类模版参数列表有了默认参数之后,在声明对象时,如果使用默认参数列表的类型,
///则在显示指定类型时可以省略
Animal<string> cat("喵喵",2);
6.3.3 类模版成员函数创建时机
类模版中成员函数和普通类中成员函数创建时机的区别:
- 普通类中的成员函数一开始就可以创建
- 类模版中的成员函数在调用时才可以创建
6.3.4 类模版对象做函数参数
传入方式:
- 指定传入类型:直接显示对象的数据类型
- 参数模版化:将对象中的参数变为模版进行传递
- 整个类模版化:将这个对象类型模版化进行传递
///指定模版参数传入类型
void printf1(Person<string, int> &p){
p.toString();
}
///参数模版化
template <class T,class W>
void printf2(Person<T, W> &p){
p.toString();
}
///整个类模版化
template <class T>
void printf3(T &t){
t.toString();
}
void test09(){
Person<string, int> zhangsan("张三", 32);
///指定传入类型
printf1(zhangsan);
///参数模版化
Person<string, int> lisi("李四", 24);
printf2(lisi);
///整个类模版化
Person<string, int> wangwu("王五",45);
printf3(wangwu);
}
6.3.5 类模版与继承
注意事项:
- 当子类继承的父类是一个类模版,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果像灵活指出父类中T的类型,子类也需要变成类模版
template <class T>
class Company{
public:
T companyName;
};
///子类继承的父类是一个类模版,需要显示指定类的数据类型
class Department:public Company<string>{
string departmentName;
};
///如果想要灵活的继承一个类模版,子类也可以声明成一个类模版
template <class T,class W>
class Staff: Company<T>{
public:
string staffName;
W departmentName;
};
6.3.6 类模版成员函数类外实现
template <class T,class W>
class Dolphin{
public:
T name;
W age;
Dolphin(T name,W age);
void toString();
};
///类模版的构造函数的类外实现
template <class T,class W>
Dolphin<T,W>::Dolphin(T name,W age){
this->name = name;
this->age = age;
}
///类模版的成员函数的类外实现
template <class T,class W>
void Dolphin<T, W>::toString(){
cout << this->name << this->age << "岁了" << endl;
}
6.3.7 类模版分文件编写
类模版成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决方案:
- 直接包含.cpp源文件
- 将声明和实现写到同一个文件中,并更改后缀名为
.hpp
,.hpp
是约定的名称,可以自定义;.hpp
文件内包含类模版声明和实现
6.3.8 类模版与友元
全局函数 类内实现 -直接在类内声明友元
全局函数 类外实现 -需要提前让编译器知道全局函数的存在
///全局函数 类外实现
///加空模版参数列表
///如果是全局函数是类外实现 需要让编译器提前知道这个函数的存在
template <class T1,class T2>class Teadcher;
template <class T1,class T2>
void printfTearch2(Teadcher<T1,T2> t){
cout << "姓名=" << t.name << " " << "年龄=" << t.id << endl;
}
template <class T1,class T2>
class Teadcher{
//全局函数 类内实现
friend void printfTearch(Teadcher<T1,T2> &t){
cout << "姓名=" << t.name << " " << "年龄=" << t.id << endl;
}
//全局函数 类外实现
friend void printfTearch2<>(Teadcher<T1,T2> t);
private:
T1 name;
T2 id;
public:
Teadcher(T1 name,T2 id){
this->name = name;
this->id = id;
}
};
void test11(){
Teadcher<string, int> zhangsan("张三",32);
//全局函数 类内实现
printfTearch(zhangsan);
//全局函数 类外实现
printfTearch2(zhangsan);
}
6.3.9 类模版案例
实现一个通用的数组类,具体描述如下:
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提高对应的拷贝构造函数以及operate =(重载操作符=)防止浅拷贝问题
- 提高尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组容量
#include <iostream>
#include <string>
using namespace std;
template<class T> class TempArray{
private:
T *array;//数组
int length;//数组初始化长度
int num;//数组当前元素个数(最后一个元素下标-1)
public:
TempArray(){
}
/// Description 构造一个T型数据类型数组,初始化大小为length
/// - Parameter length: 数组初始化长度
TempArray(int length){
this->num = 0;
this->length = length;
this->array = new T[this->length];
}
/// Description 自定义拷贝构造函数
/// - Parameter array: 拷贝的对象
TempArray(const TempArray& copy){
this->length = copy.length;
this->num = copy.num;
this->array = new T[this->length];
for(int i=0;i<this->length;i++){
this->array[i] = copy.array[i];
}
}
/// Description 重载运算符=,防止浅拷贝发生的问题
/// 返回当前TempArray对象引用,可以作为左值使用
/// - Parameter copy: 需要复制的对象
TempArray& operator = (const TempArray ©){
if(this->array != NULL){
delete[] this->array;
this->array = NULL;
}
this->length = copy.length;
this->num = copy.num;
this->array = new T[this->length];
for(int i=0;i<this->length;i++){
this->array[i] = copy.array[i];
}
return *this;
}
/// Description 重载操作符[]
/// - Parameter index: 需要获取元素的下标
T& operator[](int index){
if(index >= length){
return NULL;
}
return this->array[index];
}
/// Description 往数组内插入元素,从尾部插入
/// - Parameter &t: 需要插入的模版数据类型
void insertNode(const T &t){
if(this->num > this->length - 1){
cout << "数组已满!!!" << endl;
return;
}
this->array[this->num++] = t;
}
/// Description 删除数组内最后一位元素
void deleteNode(){
if(this->num <= 0){
cout << "数组已空!!!" << endl;
return;
}
this->num--;
}
/// Description 获取元素当前个数
int getArraySize(){
return this->num;
}
/// Description 获取数组长度
int getArrayLength(){
return this->length;
}
/// Description 获取当前下标指向的元素
T getCurrent(){
int index = this->num - 1;
return this->array[index];
}
/// Description 获取指定下标指向的元素
T getNode(int index){
return this->array[index];
}
~TempArray(){
if(this->array != NULL){
delete[] this->array;
this->array = NULL;
}
}
};
class Person{
private:
string name;
int age;
public:
Person(){
}
Person(string name,int age){
this->name = name;
this->age = age;
}
void toString(){
cout << this->name << this->age << "岁了" << endl;
}
string getName(){
return this->name;
}
int getAge(){
return this->age;
}
};
void execute(){
TempArray<int> array(10);
array.insertNode(10);
array.insertNode(20);
// int size1 = array.getArraySize();
// array.deleteNode();
// int size2 = array.getArraySize();
// cout << size1 << " " << size2 << endl;
int node = array.getCurrent();
cout << node << endl;
// int length = array.getArrayLength();
// cout << length << endl;
TempArray<int> zhangsan(10);
zhangsan = array;
TempArray<int> lisi(array);
int zhangsan_node1 = zhangsan.getCurrent();
int zhangsan_node2 = zhangsan.getNode(0);
int lisi_node1 = zhangsan.getCurrent();
int lisi_node2 = zhangsan.getNode(0);
cout << zhangsan_node1 << endl;
cout << zhangsan_node2 << endl;
cout << lisi_node1 << endl;
cout << lisi_node2 << endl;
}
void testPerson(){
TempArray<Person> student(10);
cout << "当前数组元素个数:" << student.getArraySize() << endl;
student.insertNode(Person("张三",22));
student.insertNode(Person("李四",24));
student.insertNode(Person("王五",35));
cout << "当前数组元素个数:" << student.getArraySize() << endl;
student.deleteNode();
cout << "当前数组元素个数:" << student.getArraySize() << endl;
Person lisi = student.getCurrent();
Person wangwu = student.getNode(0);
cout << lisi.getName() << endl;
cout << wangwu.getName() << endl;
}
int main(int argc, const char * argv[]) {
//execute();
testPerson();
return 0;
}