c++
一、基础
(一)C++初识
1.注释
// 1.单行注释,上方或末尾
/*
2.多行注释,上方
*/
/*
3.main是一个程序的入口
每个程序都有必须有这么一个函数
有且仅有一个
默认return 0,程序状态正常
*/
// ;是语句结束
// ;;;;多个空语句
// l+(r-1)/2比(l+r)/2比l+r>>1 更安全,精度更高,整数溢出
2.变量
数据类型 变量名 = 变量初始值
3.常量
作用:用于记录程序中不可更改的数据
1. #define 宏常量: #define 常量名 常量值
- 通常在文件上方定义,表示一个常量,编译之前,不安全
2. const修饰的变量: const 数据类型 常量名 = 常量值
- 通常在变量定义前加关键字const,修饰该变量,不可修改
4.关键字
- 不要用关键字给变量或者常量起名称
5.标识符命名规则
- 标识符不能是关键字
- 只能由字母、数字、下划线组成
- 第一个字母必须为字母或下划线
- 标识符中字母区分大小写
- 函数体以外不用下划线开头
- 不能连续两个下划线或者下划线+大写字母开头,这些被保留
- 变量一般用小写字母
- 自定义类名一般以大写字母开头
- 多单词一般下划线或者后面首字母大写
(二)数据类型
1.整型
short 2字节
int 4字节
long win 4字节,linux 4字节(32位),8字节(64位)
long long 8字节
可以自定义
unsigned int 无符号整形
字面值常量
- 0开头八进制
- 0x开头16进制
- 默认不加,int
- l或L,long
- ll或LL,long long
- u或U,unsigned 可以和L或LL组合使用
- 9527uLL
2.sizeof关键字
- 统计数据类型所占内存大小
seizeof 数据类型/变量
3.实型(浮点型)
- 默认为double,float后加f,小数默认显示6位有效数字
float 4字节 7位有效
double 8字节 15-16位有效
- 科学计数法
float f1 = 3e2 //3*10^2
float f2 = 3e-2 //3*0.1^2
4.字符型
char ch = 'a';
//单引号内只能有一个字符,不能是字符串
ch = 97
- c和c++中只占用1个字节
- 不存储字符本身,而是存放对应ASCLL编码到存储单元
- 可以直接用ASCLL给字符变量赋值
- 0-31控制字符,32-126打印字符
5.转义字符
- 用于表示一些不能显示出来的ASCLL字符
\t 8个空格
6.字符串型
- c风格字符串:
char 变量名[] = "字符串值"
- c++风格字符串:
#include <string>
string 变量名 = "字符串值"
7.布尔类型
- true 真(本质是1,非0也为真)
- false 假 (本质是0)
- 占用1个字节
8.数据的输入
cin >> 变量
cin.get() //等待键盘输入
(三)运算符
1.算术运算符
- 两个小数不可以做取模运算,只能是整型
- a%b=c,-a%b=-c
2.赋值运算符
- 左边必须是可修改的左值
- 满足右结合律,可以连续赋值
3.关系运算符
- 算术运算符优先级一般高于关系运算符
- 返回值为布尔类型
4.逻辑运算符
- 如果将算术类型的对象作为逻辑运算符的操作数,那么0为false,非0为true
- 逻辑与和逻辑或短路求值
5.条件运算符
- 可连续使用,满足右结合律
- 等同于if…else…
6.位运算符
- 之前运算符以字节为单位操作,位运算符具体操作每一位
(1)移位运算符
- 较小的整数类型(char、short、bool)会自动提升成int类型再做移位,结果为int
- 左移运算符将操作数左移之后,右侧补0
- 右移无符号补0,有符号看情况
(2)位逻辑运算符
- 先提升到int,再操作
- 异或,aa=0,a0=a,例如找只出现一个的数
7.类型转换
- 初始化,赋值,右值转换左值类型
- 隐式类型转换,长度较小的转成长度较大的类型
- 应避免大转小
- 强制类型转换
c语言风格:
(类型名称)值
c++风格:
类型名称(值)
c++强制类型转换运算符:
通常使用static_cast<类型名称>(值)
(四)流程控制语句
- swith
swith(表达式,先求值,再转换成整形){
case 整型值或常量表达式1:
...
default:
}
- c++11范围for循环
for(声明:序列表达式)
语句
for(int num : {3,6,8,10}){
cout<<num<<endl;
}
(五)复合数据类型
1.数组
(1)定义
数据类型 数组名[元素个数]
- 元素个数必须确定
- 对数组做初始化后,默认补0,未做初始化,数值都是未定义的(0xcccc,烫)
(2)访问和遍历
sizeof(a)/sizeof(a[0])
(3)多维数组
-
列数不能省略
-
遍历
for(auto& row : ia){ for(auto num : row){ } }
(4)数组的简单排序
- 选择排序
- 冒泡排序
2.模板类vector简介
#include<vector>
using namespace std;
//默认初始化,空的容器
vector<int> v1;
//列表初始化(拷贝初始化)
vector<char> v2 = {'a','b'};
vector<char> v3{'a','b'};
//直接初始化
vector<short> v4(5);//5个长度,默认0
vector<long> v5(5,100);//5个长度,默认100
//遍历
v5.size(); //v的长度
//添加元素
v5.push_back(69);
c++11增加了array模板类,固定长
3.字符串
(1)标准库类型string
#include<string>
using namespace std;
//默认初始化,空字符
string s1;
//拷贝初始化
string s2 = s1;
string s3 = "Hello";
//直接初始化
string s4("Hello");
string s5(8,'h'); //8个h连在一起
//长度
s4.size()
//字符串拼接
s3+s4
- 两个字符串字面值常量,不能相加
- 多个连续相加,左结合律,每次相加必须保证至少有一个string对象
//比较字符串
- 长度相同,字符也相同,相等
- 长度不等,较短字符串每个字符和较长对应位置字符相同,则较短字符串小于较长字符串
- 从某一位置开始不同,则比较这两个字符的ASCLL码,判别大小
(2)字符数组(C风格字符串)
- char[]类型
- c语言规定:必须以空字符结束。空字符的ASCLL码为0,专门用来标记字符串的结尾,在程序中写作’\0’
char str1[3] = {'h','e','l'}; //str1并不是一个字符串
char str2[4] = {'h','e','l','\0'} //str2是一个字符串
char str3[] = "hel"; //默认加空字符,长度为4
(3)读取输入的字符串
①使用输入操作符读取单词
- 使用iostream内置的cin对象,调用重载的输入操作符>>来读取键盘输入
- 特点:忽略开始的空白符,遇到下一个空白符(空格、回车、制表等)就会停止。
cin >> str1 >> str2
②使用getline读取一行
- 直接读取一整行输入信息
- getline函数有两个参数:一个是输入对象流cin,另一个是保存字符串的string对象;它会一直读取输入流中的内容,直到遇到换行符为止,然后把所有内容保存到srting对象中。(最后的换行符标识结束,直接丢掉)
string str;
getline(cin,str);
③使用get读取字符
- 调用cin.get()函数,不传参数,得到一个字符赋给char类型变量
- 将char类型变量作为参数传入,将捕获的字符赋值给它,返回的是istream对象
char ch;
ch = cin.get(); //将捕获到的字符赋值给ch
cin,get(ch); //直接将ch作为参数传给get
- get函数还可以读取一行内容,这种方式跟getline很相似,也可以读取一整行内容,以回车结束。主要区别在于,它需要把信息保存在一个char[]类型的字符数组中,调用的是cin的成员函数
char str[20];
cin.get(str,20)l
- get函数两个参数:一个是保存信息的字符数组,另一个是字符数组的长度
(4)简单读写文件
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main(){
ifstream input("input.txt");
ofstream output("output.txt");
/* string word;
while(input >> word){
cout << word << endl;
}
*/
/* string line;
while(getline(input,line)){
cout << line <<endl;
}
*/
char ch;
while (input.get(ch))
{
// cout << ch << endl;
output << ch << endl;
}
}
4.结构体
(1)结构体的声明,初始化
struct obj
{
type1 obj1;
type2 obj2;
...
}o1,o2={};
obj o3 = {};
obj o4 = o3;
- 创建结构体变量对象时,可以直接用定义好的结构体名作为类型;相比c语言中的定义,这里省略了关键字struct
- 如果没有赋初始值,那么所有数据将被初始化成默认值;算术类型默认值是0
(2)访问,结构体数组
- 成员运算符——点号 .
5.枚举
(1)定义
- 枚举类型enum是C++提供了另一种创建符号常量的方式,可以替代const
enum week
{
Mon, Tue, Wed = 10, Thu, Fri, Sat, Sun
};
- 与结构体不同,枚举类型内只有有限个名字,各自代表一个常量,被称为“枚举量”
- 默认情况下,会将整数值赋给枚举量
- c枚举量默认从0开始,每个枚举量依次加1;c++允许扩大范围,可以赋更多值。如上面前五个分别对应着0,1,10, 11,12,…
- 可以通过对枚举量赋值,显式的设置每个枚举量的值
Week w1 = Mon;
Week w2 = 3;//错误,类型不匹配
Week w3 = Week(3);//正确,强制类型转换
6.指针
- 是一种特殊数据类型,是一种间接访问对象的方法
- 所占字节与操作系统有关,64位为8字节
- 内存中,低位在前,高位在后
(1)定义
- 类型* 指针变量;
- 类型指的是指针所指对象的数据类型
(2)用法
- 取地址运算符&
- 解引用运算符*
- (*p).value == p->value
(3)特殊用法
①无效指针
- 定义一个指针,若未初始化,那么它内容是不确定的(比如0xcccc)。可能指向系统核心区域。也叫野指针
②空指针
- 空指针不指向任何对象
- 使用字面量nullptr,这是c++11引入的方式,推荐使用
- 使用预处理变量NULL,这是老版本的方式,本质是0
- 直接使用0值(被认为不被使用的地址,“0”地址),不能用int型赋值
③void* 指针
- 可以存放任意对象类型的地址
- 只能存放地址,不能解引用,不知道具体类型取多少字节
- 一般只用来比较地址、或者作为函数的输入输出
(4)指向指针的指针
- 指向指针的指针,**二级指针,三级指针…
- **二次解引用,三次…
(5)指针和const
①指向常量的指针
- 指针是变量,不能修改指向的数据对象,const在类型前
const int c1 = 10, c2 =25;
int* pc = &c1; //错误,类型不匹配
const int* pc = &c1;
pc = &c2;
②指针常量(const)指针
- 指针本身是一个数据对象,所以也可以区分变量和常量
- 保存的地址不能改,永远指向同一个对象
- 需要在*后、标识符前加上const
int* const cp = &i;
可以两个const
const int* const ccp = &i;
(6)指针和数组
①数组名
- 编译器一般都会把数组名转换成指针,指向第一个元素,可以用数组名给指针赋值
②指针运算
- 指针+1,根据数据类型跳到下一个对象
- 范围for循环本质就是arr+1
③指针数组和数组指针
- 指针数组:一个数组,它的所有元素都是相同类型的指针
- 数组指针:一个指针,指向一个数组的指针
int* pa[5]; //指针数组
int(* ap)[5]; //指向有五个元素的数组的指针
ap = arr; //错误,类型不匹配
ap = &arr; //正确
*ap == arr
7.引用
(1)用法
- 在c++中,起别名,叫做引用
- 在变量名前加上"&",int& ref = a
- 引用必须被初始化,绑定后就不能修改
- 不存储数据,不会被分配空间编译器直接翻译成所绑定的原始变量
- 引用的引用,int& rref =ref
(2)对常量的引用
const int zero = 0;
int& cref = zero; //错误,不能用普通引用去绑定常量
const int& cref = zero; //正确,常量的引用
常量引用初始化要求宽松,只要是可以转换成它指定类型的所有表达式,都可以用来做初始化
const int& cref2 = 10; //正确,可以用字面值常量做初始化
int i = 35;
const int& cref3 = i; //正确,可以用一个变量做初始化,拷贝
double d = 3.14;
const int& cref4 = d; //正确,d会先转成int 类型,引用绑定的是一个临时量
(3)指针和引用
- 引用也是一种间接访问方式
- 引用和指针常量相似,区别在分配内存
- 绑定指针的引用 int* & p
- 引用的本质是c++引入的一种语法糖,它是对指针的一种伪装
(六)函数
1.函数基本知识
- 函数返回类型不能是数组或者函数
(1)局部变量的生命周期
- 全局变量,程序结束时销毁
- 局部变量
- 自动对象:程序执行到变量定义语句时创建,程序运行到块末尾时销毁,栈区
- 形参也是一种自动对象
- 默认值是随机的,比如0xcccc
- 静态对象:static,局部静态对象
- 生命周期延长,程序结束时才销毁
- 只有局部的作用域,在块外依然是不可见的
- 初始化只会执行一次
- 和自动对象存放内存区域不同
- 基本类型会默认初始化为0
- 自动对象:程序执行到变量定义语句时创建,程序运行到块末尾时销毁,栈区
(2)函数声明
- 也叫函数原型
- 一般情况下,把函数声明放在头文件中会更加方便
int square(int a);
int square(int); //可以省略形参名字
2.参数传递
- 值传递,值拷贝,包括传指针
- 传引用,避免拷贝,如果不修改可以用常量引用做形参,还可以扩大传参范围
- 数组形参,数组不允许做直接拷贝
void printArray(const int*);
void printArray(const int[]); //两种方式一样,解析成指针,指定长度无效
长度如何确定
- 规定结束标记
- 把数组长度作为形参
使用数组引用作为形参
void printArry(const int(& arr)[5])
- 可变形参
- 省略号(…),兼容C语言的用法,只能出现在形参列表的最后一个位置
- 初始化列表initializer_list,跟vectr类似,也是一种标准库模板类型;initializer_list对象中的元素只能是常量值,不能更改
- 可变参数模版,这是一种特殊的函数
3. 返回类型
(1)返回类型
-
返回类型为void时,会自动加上return返回
-
原理:函数会在调用点创建一个“临时量”,用来保存函数调用的结果。当return语句返回时,就会用返回值去初始化这个临时量。所以返回值的相关规则,跟变量或者形参的初始化是一致的。
-
const string& longStr(const string& str1, const string& str2)
-
函数返回引用类型时,不能返回局部对象的引用;同样道理,也不应该返回指向局部对象的指针
-
返回类对象后连续调用,调用运算符()和点. 同级,满足左结合律
-
返回数组指针
int(*fun(int x))[5]; //函数声明,fun返回值类型为数组指针,数组长度为5 typedef :定义类型别名 typedef int arrayT[5]; //自定义类型别名,arrayT代表长度为5的int数组 arrayT* fun2(int x); //fun2的返回类型是指向arrayTd的指针 c++11尾置返回类型 auto fun3(int x) -> int(*)[5];
4.递归
- 每次递归需要保存上下文
(七)函数高阶
1.内联函数
- c++新增特性,省去函数调用,调用点代码直接替换,大大提高运行效率
- 只需在函数声明或者函数定义前加上inline关键字
- 在C中类似功能是通过预处理语句#define定义宏来实现
- 宏本身并不是函数,无法进行值传递
- 宏本质是文本替换,一般只用宏定义常量
- 宏实现函数功能比较麻烦,可读性差,一般使用内联函数取代宏
2.默认实参
-
定义默认实参,形式上就是给形参做初始化
-
一旦某个形参被定义了默认实参,那它后面的所有参数都必须有默认实参,一般在末尾
-
以传入实参为主
3.函数重载
(1)定义重载函数
- 名字 相同但形参列表不同,这是C++相对于C语言的重大改进,也是面向对象的基础
- 形参数量不同或类型不同
- 主函数不能重载
(2)有const形参时的重载
-
顶层const,常量作为形参,跟不加const完全等价
-
底层const,引用传递,只能用常量引用来绑定常量,只能用常量指针来指向常量的地址
- const限制了间接访问的数据对象是常量
- 当实参是常量时,不能对不带const的引用进行初始化,只能调用常量引用做形参的函数
- 如果实参是变量,就会优先匹配不带const的普通引用
(3)函数匹配
① 候选函数
- 与调用函数同名
- 函数的声明,在函数调用点是可见的
②可行函数
- 形参个数与调用传入的实参数量相等,默认参数可以忽略
- 每个实参的类型与对应形参的类型相同,或者可以转换成形参的类型
③寻找最佳匹配
- 实参类型与形参类型越接近,他们就匹配的越好
④多参数的函数匹配
- 逐个比较每个参数
- 如果可行函数的所有形参都能够精确匹配实参,那么它就是最佳匹配
- 如果没有全部精确匹配,那么当一个可行函数所有参数的匹配,都不比别的可行函数差、并且至少有一个参数要更优,那它就是最佳匹配
⑤二义性调用
- 如果检查所有实参之后,有多个可行函数不分优劣、无法找到一个最佳匹配,那么编译器会报错,这被称为二义性调用
(4)重载与作用域
- 如果内外层作用域都声明了同名函数,外层会被隐藏
- 不同的作用域中,是无法重载函数名的
4.函数指针
(1)声明
- 本质还是指针
- 函数的类型是由它的返回类型和形参类型共同决定的
string(*fp)(string,int,double) = nullptr;
const string& (*fp1)(const string&,const string&)
(2)使用函数指针
- 函数名后跟调用操作符(小括号)表示调用
- 单独使用函数名作为一个值时,函数会自动转换成指针,跟数组名类似
- 函数指针完全可以当做函数来使用
fp = &stuInfo; //取地址符是可选地,和下面无区别
fp = stuInfo; //直接将函数名作为指针赋给fp
(*fp)(); //fp做解引用可以得到函数
fp(); //解引用符是可选的
(3)函数指针作为形参
- 如比大小传入比较函数,形参函数指针
const string& (*fp)(const string&,const string&) //形参中*可省略,编译器自动补充,下面等价
const string& fp(const string&,const string&)
typedef const string& Func(const string&, const string&); //自定义函数类型,Func
typedef const string& (*Funcp)(const string&, const string&); //自定义函数指针类型,Funcp
- C++11提供decltype函数直接获取类型,更加简洁
typedef decltype(longerStr) Func;
typedef decltype(longerStr) *Funcp;
(4)函数指针作为返回值
- 函数不能直接返回另一个函数,但是可以返回函数指针
- 函数的返回类型必须是函数指针,而不能是函数类型
- 可以采用c++11尾置返回类型
auto fun(int) -> Funcp;;
二、C++进阶
(一)内存分区模型
- C++程序在执行时,将内存大方向划分为四个区域,赋予不同的生命周期
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
1.程序运行前
- 在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
- 代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
- 全局区
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量也存放再次
- 该区域的数据在程序结束后由操作系统释放
- 代码区:
- 全局区中存放全局变量、静态变量(static)、常量
- 常量区中存放const修饰的全局常量和字符串常量
2.程序运行后
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
- 堆区:由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- C++中主要利用new在堆区开辟内存
3.new操作符
- 堆开辟的数据,利用操作符delete
- new 数据类型
- 利用new创建的数据,会返回该数据对应的类型的指针
- deldete[] arr;释放堆区数组要加[]
(二)类和对象
- C++面向对象的三大特性为:封装、继承、多态
1.封装
(1)封装的意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
- public 公共权限
- 成员内可以访问
- 类外可以访问
- protected 保护权限
- 类内可以访问
- 类外不可以访问
- 子类可以访问父类中的保护内容
- private 私有权限
- 类内可以访问
- 类外不可以访问
- public 公共权限
(2)struct和class区别
- 唯一区别默认的访问权限不同
- struct 默认权限为公共
- class 默认权限为私有
(3)成员属性设为私有
- 可以自己控制读写权限
- 对于写权限,可以检测数据的有效性
2.对象的初始化和清理
(1)构造函数和析构函数
- 编译器自动调用,如果不提供构造和析构,编译器会提供空实现的构造和析构,析构顺序:先进后出
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值
- 类名(){}
- 没有返回值也不写void
- 函数名称与类名相同
- 可以有参数,因此可以重载
- 无需手动调用,而且只会调用一次
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
- ~类名(){}
- 没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号~
- 不可以有参数,因此不能重载
- 无需手动调用,而且只会调用一次
(2)构造函数的分类及调用
-
两种分类方式
- 按参数分为:有参构造和无参构造(默认构造)
- 按类型分为:普通构造和拷贝构造
- 拷贝构造,传入常量引用
-
三种调用方式
- 括号法
Person p1; //默认构造函数调用 Person p2(10); //有参构造函数 Person p3(p2); /拷贝构造函数 调用默认构造函数时候,不要加(),编译器会认为是一个函数的声明
- 显示法
Person p2 = Person(10); Person p3 = Person(p2); Person(10); //匿名对象,当前行执行结束后,系统会立即回收掉匿名对象 Person(p3); //报错,不能利用拷贝构造函数初始化匿名对象编译器会认为Person(p3) === Person p3; 对象声明
- 隐式调用法
Person p4 = 10; //相当于写了Person p4 = Person(10); 有参构造 Person p5 = p4; //拷贝构造
(3)拷贝构造函数调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象,会返回新的对象
(4)构造函数调用规则
- 默认情况下,c++编译器至少给一个类添加四个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
- 构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
(5)深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作
- 指针,堆区的内存可能重复释放
- 深拷贝:在堆区重新申请空间,进行拷贝操作
(6)初始化列表
-
Person(int a,int b,int c):m_A(a),m_B(b),n_C(c){ }
(7)类对象作为类成员
- 先构造类成员,再构造本身
- 先析构本身,再析构类成员
(8)静态成员
- 静态成员就是在成员变量和成员函数前加上static关键字,称为静态成员
- 静态成员变量
- 所有对象共享一份数据,不属于某个对象
- 通过对象进行访问
- 通过类名进行访问
- 也有访问权限,类外不能访问私有
- 在编译阶段分配内存
- 类内声明,类外初始化
- 所有对象共享一份数据,不属于某个对象
- 静态成员函数
- 所有对象共享一个函数
- 通过对象访问
- 通过类名访问
- 也有访问权限,类外不能访问私有
- 静态成员函数只能访问静态成员变量
- 不能确定是哪一个非静态
- 所有对象共享一个函数
3.C++对象模型和this指针
(1)成员变量和成员函数分开存储
- 在C++中,类内的成员变量和成员函数分开存储
- 只有非静态成员变量才属于类的对象上
- 空对象占用内存为1,C++编译器会给每个空对象分配一个字节空间,以区分空对象占内存的位置
(2)this指针概念
- 每个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
- this指针是隐含每一个非静态成员函数内的一种指针
- this本质是指针常量
- 指向被调用的成员函数所属的对象,链式编程思想
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
(3)空指针访问成员函数
-
c++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
-
如果用到this指针,需要加以判断保证代码的健壮性
class{ ... int age; void ShowPerson(){ if(this==NULL) return; cout <<"woshi"<<age<<endl; //age默认省略,this->age } ... };
(4)const修饰成员函数
-
常函数
- 成员函数后加const
- 修饰的是this指向,让指针指向的值也不可以修改
- 常函数内不可以修改成员属性
- 成员属性声明是加关键字mutable后,在常函数和常对象中依然可以修改
mutable int m_B; void showPerson() const{ this->m_B=100 }
- 成员函数后加const
-
常对象
- 声明对象前加const
- 常对象只能调用常函数
4.友元
-
关键字为 friend
-
让一个函数或者类,访问另一个类中私有成员
-
三种实现:
- 全局函数做友元
void gooGay(Building *building){} class Building{ friend void gooGay(Building *building); };
- 类做友元
class GoodGay{ public: GoodGay(); Building *buinding; void visit(); }; class Building{ friend class GoodGay; }; //类外写成员函数 GoodGay::GoodGay(){}
- 成员函数做友元
friend void GoodGay::visit();
5.运算符重载
-
对已有的运算符重新定义,赋予其另一种功能,以适应不同到的数据类型
-
operator运算符替换函数名
- p1+p2本质p1.operator+(p2)
- 可以发生函数重载
-
对于内置的数据类型的表达式的运算符是不可能改变的
(1)左移运算符重载
- 可以输出自定义的数据类型
- 通常不会利用成员函数重载<<运算符,因为无法实现cout在左侧,p.op<<cout
- 只能用全局函数重载<<运算符
void operator<<(ostream& cout,Person&p){} //本质cout<<p,不能链式
ostream& operator<<(ostream& cout,Person&p){
return cout;
}
- 重载左移运算符配合友元可以实现输出自定义数据类型
(2)递增运算符重载
- 实现自己的整型数据
- 前置++
MyInteger& operator++(){ //前置++,返回引用为了一直对同一个数据操作
m_Num++;
return *this;
}
- 后置++
MyInteger operator++(int){ //int代表占位参数,可以用于区分前置和后置递增
MyInteger temp = *this;
m_Num++;
return temp;
}
(3)赋值运算符重载
- 编译器默认提供浅拷贝
Person& operator=(Person &p){
if(m_Age!=NULL){
delete m_Age;
m_Age = NULL;
}
m_Age = new int(p->m_Age);
return *this; //链式编程
}
(4)关系运算符
- 让两个自定义类型对象进行对比操作
bool operator==(Person& p){}
(5)函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyAdd{
public:
int operator()(int v1,int v2);
};
MyAdd myAdd;
myAdd(1,2); //仿函数
MyAdd(); //匿名对象
MyAdd()(10,2); //匿名函数对象,重载了小括号
6.继承
(1)继承的基本语法
-
减少重复代码
-
class 子类 : 继承方式 父类
-
子类、派生类
-
父类、基类
(2)继承方式
- 公共继承
- 保护继承
- 私有继承
(3)继承中的对象模型
- 子类中所有非静态成员属性都会被子类继承下去,只是被编译器隐藏了访问不到
- 利用开发人员命令提示工具查看对象模型:
cl /d1 reportSingleClassLayout类名 文件名
(4)继承中构造和析构顺序
- 先构造父类,再构造子类
- 先析构子类,再析构父类
(5)继承同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
- 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数,想访问需要加作用域
(6)继承同名静态成员处理方式
- 与非静态一致
- 通过类名访问
(7)多继承语法
- C++允许一个类继承多个类
class 子类 : 继承方式 父类1, 继承方式 父类2,...
- 多继承可能会引发父类中有同名成员出现,需要加作用域区分
- 实际开发中不建议使用多继承
(8)菱形继承
- 两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被称为菱形继承,或者钻石继承
- 子类继承两份相同问题,导致资源浪费以及毫无意义
- 继承之前加上virtual变为虚继承,Animal称为虚基类
- 利用虚继承,解决菱形继承的问题
- 继承两个指针,通过偏移量找到唯一数据
vbptr:
v - virtual
b - base
ptr - pointer
该指针指向vbtable,记录偏移量
7.多态
(1)多态的基本概念
-
分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
-
区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
-
重写:函数返回类型、函数名、参数列表完全相同
-
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
-
动态多态的使用:
- 父类的指针或引用指向子类对象
-
优点:代码组织结构清晰;可读性强;利于前期和后期的扩展以及维护
(2)多态的原理
-
函数添加virtual后变成虚函数,类的结构发生改变,会添加一个指向 虚函数表
vftable
的 虚函数(表)指针vfptr
-
表内记录虚函数的地址,
&Animal::speak
-
当子类重写父类的虚函数时,子类中的 虚函数表内部 会替换成 子类的虚函数地址,
&Cat::speak
-
当父类的指针或者引用指向子类对象的时候,发生多态
(3)纯虚函数和抽象类
- 纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0 ;
- 当类中有了纯虚函数,这个类也称为抽象类
- 抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
(4)虚析构和纯虚析构
- 问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
- 解决方式:将父类中的析构函数改为虚析构或者纯虚析构
- 共性:
- 可以解决父类指针释放子类对象
- 都需要具体的函数实现
- 区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
virtual ~类名(){} //虚析构
virtual ~类名() = 0; //纯虚析构
类名::~类名(){} //纯虚析构需要定义也需要实现,析构类的时候会调用
(三)文件操作
1.文本文件
(1)写文件
-
- 包含头文件
#include <fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径",打开方式)
,可以省略该步直接在上一步传参 - 写数据
ofs<<"写入的数据";
- 关闭文件
ofs.close();
- 包含头文件
- 文件的打开方式可以配合使用,利用|操作符
(2)读文件
-
ifstream
-
ifs.open,ifs.is_open()判断是否打开成功
-
ifs.close
-
四种读数据方式
2.二进制文件
(1)写文件
- 打开方式要指定为
ios::binary
- 主要利用流对象调用成员函数write
ofstream& write(const char* buffer,int len);
- 字符指针buffer指向内存中一段存储空间,len是读写的字节数
(2)读文件
- 打开方式要指定为
ios::binary
- 需要判断文件是否打开成功
- 主要利用流对象调用成员函数read
ifstream& read(char* buffer,int len);
- 字符指针buffer指向内存中一段存储空间,len是读写的字节数
三、C++提高
(一)模版
1.模板的概念
-
建议通用的模具,大大提高复用性
-
不可以直接使用,它只是一个框架
-
泛型编程,主要利用技术就是模板
-
C++提供两种模板机制:函数模板和类模板
2.函数模板
(1)语法与注意事项
- 建立一个通用函数,其函数返回值类型和参数类型可以不具体制定,用一个虚拟的类型来代表
template<typename T> / template<class T>
函数声明或定义
template -声明创建模板
typename -表明其后面的符号是一种数据类型,可以用class代替
T -通用的数据类型,名称可以替换,通常为大写字母
- 使用函数模板有两种方式:自动类型推导、显示指定类型
- 注意事项:
- 自动类型推导,必须推导出一致的数据类型T,才可以使用
- 模版必须要确定出T的数据类型,才可以使用
(2)普通函数与函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模版调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
(3)普通函数与函数模板的调用规则
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
- 总结:既然提供了函数模板,就不要提供普通函数,否则容易出现二义性
(4)局限
- 问题:数组、自定义类型
- 解决:运算符重载、类型具体化
- 利用具体化模板,可以解决自定义类型的通用化
- 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板
3.类模板
(1)语法
- 建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表
template<typename T,...> / template<class T1,class T2,...>
类
(2)类模板与函数模板区别
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
(3)类模板中成员函数创建时机
- 类模板中的成员函数和普通类中成员函数创建时机是有区别的
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
(4)类模板对象做函数参数
-
类模板实例化出的对象,向函数传参的方式
- 指定传入的类型 ——直接显示对象的数据类型(最常用)
void printPerson1(Person<string,int>&p){}
- 参数模板化——将对象中的参数变为模板进行传递
template<class T1,class T2> void printPerson2(Person<T1,T2>&p){}
- 整个类模板化——将这个对象类型模板化进行传递
template<class T> void printPerson3(T &p){}
(5)类模板与继承
- 当子类继承的父类是一个类模板是,子类在声明的时候,要指定出父类中的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
(6)类模板成员函数类外实现
template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age){}
(7)类模板分文件编写
- 问题:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
- 解决:
- 直接包含.cpp源文件
- 将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
(8)类模板与友元
- 全局函数类内实现——直接在类内声明友元即可
- 全局函数类外实现——需要提前让编译器知道全局函数的存在
template<class T1, class T2>
class Person;
template<class T1, class T2>
void printPerson2(Person<T1,T2> p){} //类外实现,提前知道函数存在,还需要提前知道类存在
template<class T1, class T2>
class Person{
friend void printPerson(Person<T1,T2> p){} //全局函数,类内实现
friend void printPerson2<>(Person<T1,T2> p); //全局函数,类外实现,不加<>是普通函数
}
四、C++11新特性
(一)右值引用
1.右值和右值引用
- 左值——可取地址
- 右值——不可取地址
- 右值分为
- 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等
- 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值,std:move的返回值等
//左值
int num = 9;
//左值引用
int& a = num;
//右值引用
int&& b = 8;
//常量左值引用
const int& c = num;
const int& c = b;
const int& c = d; //右值引用给左值引用初始化
//常量右值引用
const int&& d = 6;
const int&& e = b; //错误
int&& f = b; //错误,右值引用只能由右值初始化
2.移动构造函数
- 复用其他对象中的资源(堆内存),并非所有资源
Test(Test&& a):m_num(a.m_num){
a.m_num = nullptr; //防止a析构时把内存也析构
}
Test getObj(){
Test t;
return t; //可以取地址
}
Test t = getObj(); //右侧返回的是临时量,才会调用移动构造
Test&& t1 = getObj(); //如果没有移动构造,调用拷贝构造。
return Test(); //不可以取地址
//没有移动构造函数,右侧返回临时匿名量,此方法复用所有资源
//函数返回右值引用相当于一个右值(将亡值),可以给右值引用初始化
3.&&的特性
- 未定的引用类型:
- 模版参数
T&&
- 自动类型推导
auto &&
- 模版参数
- 注意
const T&&
表示一个右值引用,不是未定引用类型 - 引用折叠:
- 通过右值推导
T&&
或者auto&&
得到的是一个右值引用类型 - 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导
T&&
或auto&&
得到的是一个左值引用类型
- 通过右值推导
- 右值传递,具名化变为左值
- 左值和右值是独立于他们的类型的,右值引用类型被推导或传递后可能是左值也可能是右值
- 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值
4.move
- 将左值转换为右值(将亡值)
- 和移动构造类似,只是转移状态或所有权,没有内存拷贝
5.forward
- 完美转发
std::forward<T>(t);
- 当T为左值引用类型时,t将被转换为T类型的左值
- 当T不是左值引用类型时,t将被转换为T类型的右值