1.内存分区模型
C++程序在执行时,将内存大方向划分为4个区域。
在程序编译后,生成exe可执行程序,未执行该程序前分为代码区和全局区两个区域
1.1 代码区
存放函数体的二进制代码,由操作系统进行管理
存放CPU执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,只读的原因是防止程序以外的修改了它的指令
1.2 全局区
存放全局变量和静态变量(static)及常量(字符串常量,const 修饰的全局变量)
注:局部常量(即const修饰的局部变量不在全局区)
该区域的数据在程序结束后由操作系统释放
1.3 栈区
由编译器自动分配释放,存放函数的参数值,局部变量等(形参数据也会分到栈区)
注:不要返回局部变量的地址
1.4 堆区
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
在C++中利用 new 在堆区开辟内存
指针本质上也是局部变量,放在栈上,指针保存的数据放在堆区
2.new运算符
利用 new 操作符在堆区开辟数据;释放时手动释放,利用操作符 delete
语法: new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
创建:int * p = new int (10);
释放:delete p;
在堆区创建与释放数组
int * p = new int [10];
delete[] p; //需要说明要释放的是数组,加 [ ]
3.引用
作用:给变量起别名
语法: 数据类型 &别名 = 原名
本质:引用的本质在C++内部实现是一个指针常量
{ int a =10;
int &ref = a; // 自动转换为 int * const ref = &a; 指针常量,指针指向不可改,也说明为什么引用不可更改
ref = 20; // 内部发现ref是引用,自动帮我们转换为 *ref = 20; 解引用 }
注意:引用必须初始化,且在初始化后,不可发生改变
3.1 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
引用传递和地址传递都可以让形参修饰实参,值传递时形参不会修饰实参。
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}// 在main中可以实现a,b值的转换
3.2 引用做函数的返回值
注意:不要返回局部变量的引用(可能第一次的结果正确,因为编译器做了保留;但在运行一次结果就会错误,因为局部变量的内存已经释放)
用法:函数调用作为左值
用法示例:
int& test() {
static int a = 10;
return a; // 返回静态变量引用 静态变量存放在全局区
}
int main() {
int& ref = test();
cout << ref << endl; // 10
test() = 1000;
cout << ref << endl; // 1000 如果函数的返回值是引用,这个函数调用可以作为左值
system("pause");
return 0;
}
3.3 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰,防止形参改变实参
{ int a =10;
int &ref = 10; // 报错,引用必须引一块合法的内存空间 }
{int a =10;
const int &ref =10; //正确 ,加上const后,编译器将代码修改为 int temp=10; int &ref = temp;}
4. 函数高级
4.1 函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的
语法: 返回值类型 函数名(参数 = 默认值)
注意事项:
- 如果我们自己传入数据,就用自己的数据;如果没有,就用默认值
- 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
- 如果函数声明有默认参数,函数实现就不能有默认参数 (会出现二义性,声明和实现只能有一个默认参数)
函数声明: int func (int a ,int b);
4.2 函数占位参数
C++中函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补该位置
语法: 返回值类型 函数名 (数据类型){ }
占位参数还可以有默认参数。 如: void fun(int a, int b =10) { }
4.3 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下(如都在全局作用下)
- 函数名称相同
- 函数参数类型不同,或个数不同,或顺序不同
注意:函数的返回值不可以作为函数重载的条件 int func(int a){};double func(int a) {} //错误
当函数重载碰到默认参数,会出现二义性,报错,尽量避免这种错误
当引用作为函数重载时, int & a 和 const int & a 也发生了函数重载
5. 类和对象-封装
C++面向对象三大特性:封装,继承,多态
万事万物皆为对象,对象上有其属性和行为
具有相同性质的对象,我们可以抽象为类,比如说人属于人类
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class 类名 {访问权限: 属性/行为 }
类中的属性和行为,我们统一称为成员
属性又叫 成员属性或成员变量 行为又叫 成员函数或成员方法
在类中,可以让另一个类作为本来中的成员
class Student
{
//权限
public:
// 属性
string name;
int ID;
//行为
void show(){ cout<<"hello"<<endl;}
};
创建一个具体学生,实例化对象:Student S1;
访问权限有三种:
- piblic 公共权限; 类内类外都可以访问
- protected 保护权限; 类内可以访问,类外不可以访问(子类可访问父类保护的内容)
- private 私有权限 类内可以访问,类外不可以访问(子类不可访问)
在C++中,struct和class唯一的区别在于 默认的访问权限不同,struct默认权限为公共,class默认权限为私有
成员属性设置为私有的优点:
- 将所有成员属性设置为私有,可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
6. 类和对象-对象特性
6.1 构造函数和析构函数
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
构造函数和析构函数会被编译器自动调用,完成对象初始化和清理工作
如果我们不提供构造和析构,编译器会提供,编译器提供的构造函数和析构函数是空实现
构造函数语法: 类名( ) { }
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次
析构函数语法: ~类名( ) { }
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
6.2 构造函数被的分类及调用
按参数分为:有参构造和无参构造(默认构造)
按类型分为:普通构造和拷贝构造 ( 语法:类名(const 类名 &对象){ } 一样的数据类型,加const修饰,引用的方式)
三种调用方式:括号法( Person p1(p2) ),显示法( Person p1 = Person(p2) ),隐式转换法( Person p4 = 10)
注意事项:
- 调用默认构造函数时,不要加(),因为编译器会认为是一个函数的声明,不会认为在创建对象,如Person p();
- Person(10),这种类型被叫做匿名对象,特点是当前行执行结束后,系统会立即回收掉匿名对象
- 不要利用拷贝构造函数,初始化匿名对象,如Person (p2); 这样 编译器会认为Person (p2) == Person p2,这是一个对象声明,会显示重定义的报错
6.3 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不提供默认无参构造,但会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
6.4 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作 (存在问题: int *height; 编译器会在默认拷贝构造中实现 height = p1.height,会出现堆区内存重复释放的问题)
深拷贝:再堆区重新申请空间,进行拷贝操作
浅拷贝带来的问题是 堆区的内存重复释放——解决方法:深拷贝(自己实现拷贝构造函数 height = new int (*p1.height))
析构代码,可以将堆区开辟的数据做释放操作,具体为:假设在类中定义 int *height
则在最后释放时,需要在析构函数中添加代码:
if(height ! = NULL){
delete height;
height = NULL;
}// 仍需要自己实现拷贝构造函数,解决浅拷贝带来的问题
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
6.5 初始化列表
作用:用来初始化属性
语法: 构造函数():属性1(值1),属性2(值2)... { }
6.6 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
如: class A { }; class B { A a },则B类中有对象A作为成员,A为对象成员
当其他类对象作为本类成员,构造时候先构造类对象,再构造自身;析构时先析构自身,再析构类对象(析构的顺序与构造相反)
6.7 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为静态成员变量和静态成员函数
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员变量不属于某个对象上,所有对象共享同一份数据,因此静态成员变量有两种访问方式,即通过对象进行访问以及通过类名进行访问(作用域下 Person::变量)
静态成员变量也是有访问权限的,私有静态成员变量访问不到
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量,而不可以访问非静态成员变量,因为无法区分到底是哪个对象的属性
静态成员函数也是有访问权限的,类外访问不到私有静态成员函数
6.8 this指针
6.8.1 成员变量和成员函数的存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上(静态成员变量,非静态成员函数,静态成员函数均不属于类对象上)
class Person { } Person p; 则sizeof(p)的大小为1字节,即空对象占用内存空间为1,C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置,每个空对象也应该有一个独一无二的内存地址
6.8.2 this指针
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会供用一块代码,那么这一块代码是如何区分是哪个对象调用自己呢?——解决方案,this指针
this指针是一种特殊的对象指针,this指针指向被调用的成员函数所属的对象,this指针的本质是指针常量
this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
C++中空指针也是可以调用成员函数的
6.8.3 const修饰成员函数
常函数:
- 成员函数后加const,我们称这个函数为常函数 void showPerson() const { }
- 常函数内不可修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改 mutable int B;
常对象:
- 声明对象前加const称该对象为常对象 const Person p;
- 常对象只能调用常函数,不能调用普通成员函数,因为普通成员函数可以修改属性
7. 类和对象-友元
友元的目的就是让一个函数或者类,访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现:全局函数做友元;类做友元;成员函数做友元
写法:
在函数声明,类,成员函数前加 friend,把 friend 函数声明,类,成员函数放在类中的第一行
如 class Person{
friend class Buli; //类
或者 friend void Sett(int a); // 全局函数
或者 friend void 类名::Sett(int a) //成员函数
public:// 友元代码要放在权限的上面
......
}
8. 类和对象-C++运算符重载
运算符重载:对已有的运算符重新进行定义,赋予其另一种功能,已适应不同的数据类型
运算符重载,也可以发生函数重载
对于内置的数据类型的表达式的运算符是不可能改变的;不要滥用运算符重载
8.1 加号运算符重载
函数名称由编译器起名,可以使用成员函数或全局函数重载+号
若定义一个类为 class Person {
public:
Person operator+ (Person &p) {
Person temp;
temp.m_A =this->m_A+ p.m_A;
temp.m_B = this->m_B + m_B;
return temp;
}
...........
}
后续可以使用Person p3 = p1 + p2; //本质上为 Person p3 = p1.operator+(p2)
8.2 左移运算符重载
作用:可以输出自定义数据类型
当利用成员函数重载左移运算符 p.operator<< (cout) 简化版本为 p<<cout ,无法实现cout在左侧,所以我们不会利用成员函数去重载<<,只能利用全局函数去重载
如定义全局函数:
ostream &operator<< (ostream &cout, Person &p){
//本质 operator<<(cout , p) 简化为cout<< p,cout属于输出流对象
cout<< p.m_A;
cout<<p.m_B;
return cout;
}// 链式编程思想
后续可以使用 Person p; cout<<p<<endl;
8.3 递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
前置递增返回引用,后置递增返回值
在成员函数中写入
class MyInteger {
friend ostream& operator<<(ostream& cout, MyInteger myint); //对于左移运算符的重载 后面不是引用传递
public:
MyInteger() {
m_Num = 0;
}
//重载++运算符 前置++
MyInteger &operator++() { //返回引用是为了一直对一个数据进行递增操作
m_Num++; //先++
return *this; //在将自身做返回
}
//重载++运算符 后置++
MyInteger operator++(int) { //区分前置和后置递增,在括号中写入int,表示占位参数 返回值
MyInteger temp = *this; //先记录当时结果
m_Num++; // 后递增
return temp; // 最后将记录结果做返回
}
private:
int m_Num;
};
后续可以使用 MyInteger myint; cout<< ++myint <<endl;
8.4 赋值运算符重载
C++编译器至少给一个类添加4个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝完问题
在类Person中:
Person &operator=(Person &p) {
//应该先判断是否有属性在堆区,如果有先释放干净,然后在深拷贝
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
//深拷贝
m_Age=new int(*p.m_Age);
//返回对象本身
return *this;
}
int* m_Age;
后续可以使用p3 = p2 = p1; // Person p1;.....
8.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
在定义的类中:
bool operatop==(Person &p){
if(this->m_Name==p.m_Name && this->m_age==p.m_age) { return true; }
return false;
}
后续可以使用 Person p1; Person p2; if(p1 == p2) { }
8.6 函数调用运算符重载
函数调用运算符()也可以重载,称为仿函数
仿函数没有固定写法,非常灵活
void operator() (string test) { cout << test<<endl; }
9. 类和对象-继承
继承的好处:减少重复代码
语法: class 子类 : 继承方式 父类
子类也成为派生类,父类也成为基类
派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员
继承方式一共有三种:公共继承,保护继承,私有继承
若子类以公共继承方式继承父类,则
- 父类中的公共权限成员,到子类中依然是公共权限
- 父类中的保护权限成员,到子类中依然是保护权限
- 父类中的私有权限成员,子类访问不到
- 类外可以访问公共权限成员,其他访问不到
若子类以保护继承方式继承父类,则
- 父类中的公共权限成员与保护权限成员,到子类中是保护权限
- 父类中的私有权限成员,子类访问不到
- 类外不可以访问权限成员
若子类以私有继承方式继承父类,则
- 父类中的公共权限成员与保护权限成员,到子类中是私有权限
- 父类中的私有权限成员,子类访问不到
- 类外不可以访问权限成员
父类中所有非静态成员属性都会被子类继承下去,父类中的私有成员属性,是被编译器隐藏了,因此访问不到,但确实被继承下去了。
如父类中定义三种权限成员各一个,数据类型是int,子类中以公共方式继承并定义一个数据类型为int 的成员,则sizeof(子类)的大小为16字节
继承中的构造和析构顺序如下:先构造父类,在构造子类,析构的顺序与构造的顺序相反(即先析构子类,在析构父类)
9.1 继承同名成员处理
当子类和父类出现同名的成员(包括静态成员),如何通过子类对象,访问到子类或父类中同名的数据:
- 访问子类同名成员,直接访问即可 s1.m_A
- 访问父类同名成员,需要加作用域 s1.Base::m_A
class Base {
public:
Base() {
m_A = 100;
}
int m_A;
};
class Son : public Base {
public:
Son() {
m_A = 200;
}
int m_A;
};
cout << s1.m_A << endl;
cout << s1.Base::m_A << endl;
如果子类中出现和父类同名的成员函数(包括静态成员函数),子类的同名成员会隐藏掉父类中所有同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加作用域
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)
9.2 多继承语法
C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1,继承方式 父类2
多继承可能会引发父类中有同名成员出现,需要加作用域区分
实际开发中不建议用多继承
9.3 菱形继承
两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被被称为菱形继承,或者钻石继承
当菱形继承,两个父类拥有相同数据,需要加以作用域区分;这份数据其实只有一份就可以,菱形继承导致数据有两份,造成资源浪费
利用虚继承,解决菱形继承的问题,即在权限之前,加上关键字 virtual 变为虚继承
如 class Animal {};
class sheep : virtual public Animal {};
10. 类和对象-多态
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
可通过虚函数实现对地址的晚绑定,如父类中的成员函数和子类中的相同,则需要在成员函数前加virtual变成虚函数
如:class Animal {
public:
virtual void speak() { .... } // 虚函数
}
动态多态满足的条件:
- 有继承关系
- 子类重写父类的虚函数
函数重写概念: 函数返回值类型,函数名,参数列表完全相同
动态多态的使用:父类的指针或者引用,指向子类对象
如:void DoSpeak (Animal &animal ) {
animal.speak();
delete animal;
}
在测试案例中,有一个类Cat,定义Cat cat; DoSpeak(cat); // 本质是Animal &animal = cat;
或者 void DoSpeak (Animal * animal ) {
animal->speak();
}
在测试案例中,有一个类Cat,定义DoSpeak(new Cat);
10.1 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数的语法:virtual 返回值类型 函数名 (参数列表)= 0;
当类中有了纯虚函数,这个类也成为抽象类
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
如:class Base {
public:
virtual void func() = 0; //纯虚函数
};
class Son:public Base {
public:
void func() {} // 重写纯虚函数
};
10.2 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,(也可以说为 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,会出现内存泄露),解决方式是将父类中的析构函数改为虚析构或者纯虚析构,以此解决父类指针释放子类对象时不干净的问题
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象
纯虚析构,既需要声明也需要实现
虚析构语法:virtual ~类名 ( ) { }
纯虚析构语法:virtual ~类名 ( ) = 0; 类名 :: ~类名 ( ) { }
如果子类中没有堆区数据(即new 数据),可以不写为虚析构或纯虚析构
11. 文本操作
程序运行时产生的数据属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件 <fstream>
文件类型分为两种:
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
11.1 写文件步骤-文本文件
1. 包含头文件:#include <fstream>
2. 创建流对象 :ofstream ofs;
3. 打开文件:ofs.open("文件路径",打开方式);
4. 写数据:ofs<<"写入的数据"<<endl; //可以换行
5. 关闭文件:ofs.close();
打开方式:
- ios::in 为读文件而打开文件
- ios::out 为写文件而打开文件
- ios::ate 初始位置:文件尾
- ios::app 追加方式写文件
- ios::trunc 如果文件存在先删除,在创建
- ios::binary 二进制方式
文件打开方式可以配合使用,利用 | 操作符
如:用二进制方式写文件 ios::binary | ios::out
11.2 读文件步骤-文本文件
1. 包含头文件:#include <fstream>
2. 创建流对象 :ifstream ifs;
3. 打开文件并判断文件是否成功打开:ifs.open("文件路径",打开方式); if(! ifs.is_open()) {cout<<"文件打开失败"<<endl; ;return; }
4. 读数据:四种读取方式
5. 关闭文件:ifs.close();
四种读取数据方式:
第一种:
char buf [1024] = { 0 };
while ( ifs>> buf ) { cout<<buf<<endl; }
第二种:
char buf [1024] = { 0 };
while (ifs.getline(buf,sizeof(buf))) { cout<<buf<<endl; }
第三种:
string buf;
while (getline(ifs,buf)) { cout<<buf<<endl; }
第四种:
char c;
while ((c=ifs.get()) != EOF) { cout<<c; }
11.3 写文件步骤-二进制文件
1. 包含头文件:#include <fstream>
2. 创建流对象 :ofstream ofs;
3. 打开文件:ofs.open("文件路径",ios::out | ios::binary);
4. 写数据:ofs.write(); 这里假设有一个类class Person { }; Person p ={ "张三",18 }; ofs.write ( (const char *)&p,sizeof(Person) );
5. 关闭文件:ofs.close();
以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary
二进制方式写文件主要利用流对象调用成员函数 write
函数原型:ostream& write ( const char * buffer, int len );
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
11.4 读文件步骤-二进制文件
1. 包含头文件:#include <fstream>
2. 创建流对象 :ifstream ifs;
3. 打开文件并判断文件是否成功打开:ifs.open("文件路径",ios::in | ios::binary); if(! ifs.is_open()) {cout<<"文件打开失败"<<endl; ;return; }
4. 读数据 ifs.read(); 这里假设有一个类class Person { }; Person p; ifs.read ( (char *)&p,sizeof(Person) ); cout<<...<endl;
5. 关闭文件:ifs.close();
二进制方式读文件主要利用流对象调用成员函数 read
函数原型:istream& read ( char * buffer, int len );
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数