C++笔记
一、C++基础
1、程序初识
1.1常量
作用:用于记录程序中不可更改的数据
C++定义常量的两种方式
-
define宏常量:#define 常量名 常量值
通常在文件上方定义,表示一个常量
- const修饰的变量:const 数据类型 常量名=常量值
通常在变量定义前加关键字const,修饰该变量为常量,不可修改
实例
#include <iostream> //包含头文件
using namespace std; //指定缺省的命名空间
//1、宏常量
#define day 7
int main()
{
cout << "一周里总共有"<<day<<"天\n";
//day=8; 报错,宏常量不可以修改
//2、const修饰变量
const int month = 12;
cout << "一年里总共有" << month << "个月份\n";
//month=24; 报错,常量是固定的不可以被修改
return 0;
}
1.2关键字
1.3标识符
包括变量、常量、函数、结构体、类等
命名规则:
- 不能是关键字
- 只能由字母、数字、下划线组成
- 第一个字符不能是数字
- 字母区分大小写
- 给标识符名命名时尽量做到“见名知意”的效果
- 以下划线和大写字母打头的名称被保留给编译器及使用的资源使用,如果违法了这一规则会导致行为的不确定性
1.4输出数据
-
分类
-
a. 数字:直接书写,如:100、18.52
-
b. 字符:用半角的单引号包含,如:’A’、’0’、’!’、’+’
-
c. 字符串:用半角的双引号包含,如:“Hello World”、“100”
-
-
输出数据
-
std::cout可以输出各种类型的数据
-
<<操作符可以拼接多个数据项
-
std::endl或/n可以换行
-
using namespace std指定缺省的命名空间
-
1.5输入数据
1、用std::cin输入数据
语法:std::cin>>变量名
注意:
a、布尔型变量的值在计算机内部用1(true)和0(false)存储;
b、程序中可以书写true(真)和false(假),也可以书写1和0,其它值将强制转换成1
c、用cin输入时可以填1和0,其它也强制转换成1
d、用cout输出时只显示1和0,不显示true和false
e、如果输入的数据与变量的数据类型不匹配,会导致行为的不确定性
2、数据类型
基本概念:
C++用int关键字来声明整型变量(int是integer的简写)
在声明整型变量的时候,可以在int关键字之前加signed、unsigned、short和long四种修饰符
C++规定在创建一个变量或常量时,必须要指出相应的数据类型,否则无法给变量分配内存
signed:有符号的,可以表示整数和负数
unsigned:无符号的,只能表示正数
short:短的,取值范围小,占用内存少
long:长的,取值范围大,占用内存多
2.1整型
默认是十进制,一个表示十进制的数字不需要任何特殊的格式
作用:整型变量表示的是整型类型的数据
- short(短整型):2字节,-3276832767(-2^152^15-1 )
- unsigned short:2字节,0~65535
- int(整型):4字节,-21474836482147483647(-2^312^31-1)
- unsigned int:4字节,0~4294967295
- long(长整型):Win 4字节,Linux(32位为4字节、64位为8字节),-92233720368547758089223372036854775807(-2^312^31-1)
- unsigned long:8字节,0~18446744073709551615
- long long(长长整型):8字节 -263~263-1
注意:
整数的取值范围与计算机操作系统和C++语言编译器有关,没有一个固定的数值,我们可以根据它占用的内存大小来判断它的取值范围
1个位的取值是0 1 1=2^1-1
2个位的取值是00 01 10 11 3=2^2-1
3个位的取值是000 001.....111 7=2^3-1
- 1个字节有8个位,表示的数据的取值范围是2^8-1,即255
- 如果占用的内存是2个字节,无符号型取值范围是28*28-1
- 如果占用的内存是4个字节,无符号型取值范围是28*28*28*2*8-1
- 如果占用的内存是8个字节,无符号型取值范围是28*2828*2828*28*28*28-1
- 如果是有符号,取值范围减半,因为符号占1个位
- 计算机用最高位1位来表达符号(0-正数,1-负数),unsigned修饰过的正整数不需要符号位,在表达正整数的时候比signed修饰的正整数取值大一倍
- 给整数变量赋值不能超过它的取值范围,否则能产生不可预料的后果
- 在实际开发中,为了防止超出取值范围,应该保证有足够的空间
二进制
由0和1组成,书写时必须以0b或0B(不区分大小写)开头
以下是合法的二进制:
int a= 0b101; //换算成十进制为5
int b = -0b110010; //换算成十进制为-50
int c= 0B100001; //换算成十进制为33
以下是非法的二进制:
int m=101010; //无前缀0B,相当于十进制
int n=0B410; //4不是有效的二进制数字
注意:C++标准并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字,换句话说,并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关
八进制
由0~7八个数字组成,书写时必须以数字0开头
以下是合法的八进制:
int a= 015; //换算成十进制为13
int b = -0101; //换算成十进制为-65
int c= 0177777; //换算成十进制为65535
以下是非法的二进制:
int m=256; //无前缀0,相当于十进制
int n=03A2; //A不是有效的八进制数字
十六进制
由数字09、字母AF或a~f(不区分大小写)组成,书写时必须以0x或0X(不区分大小写)开头
以下是合法的十六进制:
int a= 0X2A; //换算成十进制为42
int b = -0XA0; //换算成十进制为-160
int c= 0xffff; //换算成十进制为65535
以下是非法的十六进制:
int m=5A; //无前缀0X,相当于无效数字
int n=0X3H; //H不是有效的十六进制数字
注意:
- 在C++中,不要在十进制数前面加0,会被编译器当成八进制
- 不要随便删除别人程序中整数前面的0,它不是多余的
2.2 sizeof运算符
作用:可以统计数据类型或变量所占内存大小
用于数据类型:sizeof(数据类型)
用于变量:sizeof(变量名)或sizeof 变量名
注意:
- 在32位和64位操作系统中,同一种数据类型占用的内存空间可能不一样
- 字符串(string)不是C++的基本数据类型,用sizeof求它占用内存的大小没有意义
实例:
int main()
{
cout << "short类型所占内存空间为:" << sizeof(short) << endl;
cout << "int类型所占内存空间为:" << sizeof(int) << endl;
cout << "long类型所占内存空间为:" << sizeof(long) << endl;
cout << "long long类型所占内存空间为:" << sizeof(long long) << endl;
return 0;
}
2.3实型(浮点型)
作用:用于表示小数
分类:
1、单精度float :4字节,7位有效数字
2、双精度double:8字节,15~16位有效数字
3、扩展精度long double:不少于double,不低于double
注:区别在于表示的有效数字范围不同
实例:
int main()
{
float f1 = 3.14f; //一般数字尾数加f来表示,不加默认为double类型
double f2 = 3.14;
cout << f1 << endl;
cout << f2 << endl;
//科学计数法
float d1 = 3e2; //3*10^2
cout << "d1=" << d1 << endl;
double d2 = 3e-2; //3*0.1^2
cout << "d2=" << d2 << endl;
return 0;
}
注意:
- 在VS和Linux中,long double占用的内存空间分别是8和16字节
- 有效数字包括了小数点前面和后面的数字
- C++缺省显示6位有效数字,如果要显示更多的有效数字,可以用printf()函数
- 浮点数的存储方法和整数不一样,比较复杂,如无必要,不用研究。(百度“为什么浮点数的小数位那么多”、“为什么浮点数不精确”)
- 在实际开发中,用整数代替浮点数,整数的运算更快,精度更高
2.4字符型(char)
- 字符型(char)占用的内存空间是1个字节,书写用单引号包含
- 在内存中,不存放字符本身,而是存放与它对应的编码,即是ASCII码
- ASCII(美国信息交换标准代码)是现今最通用的单字节编码方案,包含了33个控制字符(具有特殊含义无法显示的字符)和95个可显示字符
ASCII控制字符(0~31)
- 32是空格
- 48~57是0到9十个阿拉伯数字
- 65~90是26个大写英文字母
- 97~122是26个小写英文字母
- 其余的是一些标点符号、运算符号等
- 第127个字符表示的是键盘上的删除键
字符的本质:
- 字符的本质是整数,取值范围是0~127
- 在书写的时候可以用单引号包含,程序执行的时候,将把符号解释为对应的整数
- 显示的时候,把整数解释为对应的符号,也可以直接显示整数
- 可以与整数进行任何运算,运算的时候,书写方式可以用符号,也可以用整数
- C++为什么没有提供1字节的整型?
2.5转义字符
在C++程序中,使用转义字符的原因有两个:
- 控制字符没有符号,无法书写,只能用其它的符号代替
- 某些符号已被C++征用,语义冲突,只能用其他的符号代替
常用:
| ASCII码值 | 转义字符 | 含义 |
|---|---|---|
| 0 | \0 | 空,给字符型变量赋值时可以直接书写0 |
| 10 | \n | 换行(LF),将当前位置移到下一行开头 |
| 13 | \r | 回车(CR),将当前位置移到本行开头 |
| 9 | \t | 水平制表(HT)或对齐(跳到下一个TAB位置) |
| 92 | \ | 斜线 |
| 34 | \“ | 双引号,书写字符时不必转义 |
| 39 | \‘ | 单引号,书写字符串时不必转义 |
少用:
| ASCII码表 | 转义字符 | 含义 |
|---|---|---|
| 7 | \a | 警报 |
| 8 | \b | 退格(BS),将当前位置移到前一行 |
| 12 | \f | 换页(FF),将当前位置移到下页开头 |
| 11 | \v | 垂直制表(VT) |
2.6C++11的原始字面量
原始字面量(值)可以直接表示字符串的实际含义,不需要转义和连接
语法:R”(字符串的内容)"
R"xxx(字符串的内容)xxxx" //”xxx“标签,目的是提高可读性
示例:
#include<iostream>
using namespace std;
int main()
{
string path = "C:\\Program Files\\Microsoft OneDrive\\tail\\nation";
string path1 = R"abcd(C:\\Program Files\\Microsoft OneDrive\\tail\\nation)abcd";
cout << "path is" << path << endl;
cout << "path1 is" << path1 << endl;
string str = R"(
<no>0001</no>
<name>西施</name>
<sc>火树银花</sc>
<yz>沉鱼</yz>
<age>23</age>)";
cout << str << endl;
}
2.6字符串类型
C++风格字符串:string 变量名="字符串的内容";
C风格字符串:char 变量名[]="字符串的内容";
C风格字符串的本质是字符数组,C++风格字符串的本质是类,它封装了C风格字符串
C++风格字符串的常用操作:
- 赋值:变量名="字符串的内容";
- 拼接:变量名=变量名+"字符串的内容一"+"字符串的内容二"+......+"字符串的内容n";
字符串的内容要以分成多行书写,"+"可以不写
示例:
#include<iostream>
using namespace std;
int main()
{
string str = "西施"; //声明字符串变量并初始化
str = "美女西施"; //对字符串变量重新赋值
cout << "请输入超女姓名:";
cin >> str;
if (str == "冰冰")cout << "我喜欢\n"; //用==可以判断两个字符串是否相同
if (str != "冰冰")cout << "我不玩了\n"; //用!=可以判断两个字符串是否部相同
str = "姓名:" + str + ",这是我现女友"; //用+可以拼接多个字符串
//如果字符串的内容都是常量,不要写“+”,如果内容很长,可以分成多行书写
str = "超女姓名:"
"咪咪"
",这是我的前女友";
}
2.7布尔类型
- 在C和C++中,关系运算和逻辑运算的结果有两种:真和假
- C语言用0表示假,非0表示真
- C++新增了bool类型,占用1字节的内存,用true表示真,false表示假
- bool类型本质上是1字节的整数(字符),取值只有1和0
- 在程序中,书写的时候可以用true和false,编译器把它们解释为1和0
- 用cin输入和cout输出的时候,仍是1和0,不会解释为true和false
示例:
#include<iostream>
using namespace std;
int main()
{
bool a = true, b = false;
cout << "sizeof(bool)=" << sizeof(bool) << endl;
cout << "a=" << a << ",b=" << b << endl;
cout << "a+b=" << a + b << endl;
//找到布尔变量b的内存,把里面的数据强制为8
char* c = (char*)&b;
*c = 8;
cout << "b=" << b << endl;
//bool类型的本质是unsigned char
//true和false是C++在语法上的处理
}
2.8C++11的long long类型
在VS中,long是4字节,32位。 -2147483648~2147483647
在Linux中,long是8字节,64位。 -9223372036854775808~9223372036854775807
C++11标准增了long long类型的整数,至少64位,且至少与long一样长
*2.9数据类型的转换
计算机进行运算时,要求各操作数的类型具有相同的大小和存储方式
在实际开发中,不同类型的数据进行混合运算是基本需要
自动类型转换(隐式转换):某些类型的转换编译器可以隐式的进行,不需要程序员干预
强制类型转换(显式转换):有些类型的转换需要程序员显式指定
1)自动类型转换
不同数据类型的差别在于取值范围和精度,数据的取值范围越大,精度越高
整型从低到高:
char->short->int->long->long long
浮点型从低到高:
fioat->double->long double
自动类型转换的规则如下:
- 如果一个表达式中出现不同类型操作数的混合运算,较低类型将自动向较高类型转换
示例:
char a=30;
int b=102400;
long long c=150000000001
cout<<"a+b+c="<<a+b+c;
- 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。
示例:
cout<<"8/5="<<8/5.0;
- 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型
示例:
int d=23.59;
cout<<"d="<<d<<endl;
- 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋值给左值,所得结果可能毫无意义
示例:
unsigned int e = 4294967295 + 10; //值被截断
cout << "e=" << e << endl;
//4294967295 111111111111111111
//4294967296 000100000000000000
//4294967297 000100000000000001
2)强制类型转换
为了让程序设计更灵活,转换的目的更清晰,C++提供了强制类型转换的方法,也称为显式转换
语法:(目的类型)表达式或目的类型(表达式);
注意:
- 如果使用强制转换,表示程序员已有明确的目的
- 如果转换的行为不合理,后果由程序员承担
- 如果采用了强制类型转换,编译的警告信息将不再出现
- 类型转换运算符的优先级比较高,如果没把握就加括号
2.10数据类型的别名
创建数据类型的别名有两个目的:
- 为名称复杂的类型创建别名,方便书写和记忆
如:
typedef unsigned int uint;
typedef long long llong;
typedef unsigned long long ullong;
- 创建与平台无关的数据类型,提高程序的兼容性
如:
typedef short int16_t; //16位的整数
typedef int int32_t; //32位的整数
rtpedef long long int64_t; //64位的整数
一般放在头文件,在源代码中,只使用别名int16_t、int32_t、int64_t
语法:typedef 数据类型名 别名;
3、运算
3.1算术运算
| 运算符 | 描述 |
|---|---|
| + | (加法运算)两个数相加 |
| - | (减法运算)一个数减另一个数 |
| ***** | (乘法运算)两个数相乘 |
| / | (除法运算)分子除以分母 |
| % | (取模运算符),整除后的余数 |
注意:
-
整数进行除法运算时,如果分母为0,程序将异常退出
-
浮点数进行除法运算时,如果分母为0.0,将得到inf(nfinite,无穷大)
-
两个整数进行除法运算时,将舍去小数部分,得到一个整数
-
整数与浮点数进行除法运算时,得到的结果是浮点数
-
在整数前面加(float)或(double)可以将整数转换为float或double类型
如:
#include<iostream> using namespace std; int main() { string str = "西施"; //声明字符串变量并初始化 str = "美女西施"; //对字符串变量重新赋值 cout << "请输入超女姓名:"; cin >> str; if (str == "冰冰")cout << "我喜欢\n"; //用==可以判断两个字符串是否相同 if (str != "冰冰")cout << "我不玩了\n"; //用!=可以判断两个字符串是否部相同 str = "姓名:" + str + ",这是我现女友"; //用+可以拼接多个字符串 //如果字符串的内容都是常量,不要写“+”,如果内容很长,可以分成多行书写 str = "超女姓名:" "咪咪" ",这是我的前女友"; } -
取模运算只能用于整数(分母也不能为0)
3.2自增和自减
| 运算符 | 描述 |
|---|---|
| ++变量名 | 先把变量值加1,然后再执行表达式 |
| 变量名++ | 先执行表达式,再把变量的值加1 |
| --变量名 | 先把变量值减1,然后再执行表达式 |
| 变量名-- | 先执行表达式,再把变量的值减1 |
3.3赋值运算
| 运算符 | 示例 | 描述 |
|---|---|---|
| = | c=a+b | 将把a+b的值赋值给c(把右边操作数的值赋给左边操作数) |
| += | c+=a | 相当于c=c+a,加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 |
| -= | c-=a | 相当于c=c-a,减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 |
| *= | c*=a | 相当于c=c*a,乘且赋值运算符,把右边操作数乘以左边操作数的 |
| /= | c/=a | 相当于c=c/a,除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 |
| %= | c%=a | 相当于c=c%a,求余数且赋值运算符,求两个操作数的模赋值给左边操作数,浮点数不适用取余数 |
注意:
- 字符串(string)只能使用等号(=)赋值,不能使用其它的赋值运算符
- 浮点数不能使用%=运算符
- 等号(=)赋值运算符可以连续使用
- 如果等号两边操作数的数据类型不同,C++将自动转换数据类型,可能会损失精度,也可以超出取值范围,如果转换不成功,编译时会报错
3.4初始化赋
-
把值写在小括号中,等于号可以省略(C++标准)
int a = (15); //声明变量a,初始化值为15 int b(20); //声明变量b,初始化值为20`
-
把值写在花括号中,等于号也可以省略(C++标准),统一初始化列表
int a = { 15 }; //声明变量a,初始化值为15 int b{ 20 }; //声明变量b,初始化值为20
注意:在Linux平台下,编译需要加 -std=c++11参数
3.5关系运算
用于比较两个表达式的值,运算的结果为1-true和0-true
| 关系 | 数学中的表示 | C++的表示 |
|---|---|---|
| 等于 | = | == |
| 不等于 | ≠ | != |
| 小于 | < | < |
| 小于等于 | ≤ | <= |
| 大于 | > | > |
| 大于等于 | ≥ | >= |
注意:
- 关系运算符的两边可以是数值,也可以是表达式
- 用std::cout输出关系运算表达式时,要加括号
- 关系运算也适用于字符串(string)
3.6逻辑运算
根据给定的逻辑因子(表达式或值)返回一个新的逻辑因子
| 运算符 | 术语 | 示例 | 结果 |
|---|---|---|---|
| && | 逻辑与 | a&&b | 如果a和b都为真,则结果为真,否则为假 |
| || | 逻辑或 | a||b | 如果a和b有一个为真,则结果为真,都为假时,结果为假 |
| ! | 逻辑非(反) | !a | 如果a为假,则!a为真;如果a为真,则!a为假 |
注意:
-
逻辑运算符的两边可以是数值,也可以是表达式
-
用std::cout输出逻辑运算表达式时,逻辑表达式要加括号,如:
bool a=true ; //声明变量a为... bool b=false; //声明变量b为... cout <<"!a=" <<(!a)<<endl; cout << "a$$b=" << (a && b); -
在实际开发中,逻辑运算的多重组合是重点
3.6逗号运算
- 把一行语句中的多个表达式连接起来,程序将从左到右执行表达式
语法:表达式一,表达式二,...,表达式n;
- 逗号运算常用于声明多个变量,如:
int a , b; //声明变量a,b
int c = 10, d = 12; //声明变量c,d并初始化
- 也可以用于其他语句中,但是,逗号运算符是所有运算符中级别最低的,以下两个表达式的效果是不同的,如:
int a,b;
b=a=2,a*2;
b=(a=2,a*2);
3.7运算的优先级
一个表达式可以包含多个运算符,运算符的优先级决定了表达式各部分的执行顺序,例如,按照运算规则,”*****“的优先级比”+“高,所以b*c将先执行,如:
a+b*c;
如果想让”a+b“先执行,则必须加括号,如:
(a+b)*c;
如果一个表达式中的两个操作数具有相同的优先级,那么它们的结合律(associativity)决定了它们的执行顺序(从左到右或从右到左)。例如,算术运算的组合方式是从左到右,赋值运算符则是从右到左,如下:
| 表达式 | 结合律 | 组合方式 |
|---|---|---|
| a/b%c | 从左到右 | (a/b)%c |
| a=b=c | 从右到左 | a=(b=c) |
下表是全部运算符的优先级和结合律
| 优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 |
|---|---|---|---|---|
| 1 | 左到右 | |||
| [] | 数组下标 | 数组名[常量表达式] | ||
| () | 圆括号 | (表达式)/函数名(形参表) | ||
| . | 成员选择(对象) | 对象.成员名 | ||
| -> | 成员选择(指针) | 对象指针->成员名 | ||
| 2 | 右到左 | |||
| - | 负号运算符 | -表达式 | ||
| (类型) | 强制类型转换 | (数据类型)表达式 | ||
| ++ | 前置自增运算符 | ++变量名 | ||
| ++ | 后置自增运算符 | 变量名++ | ||
| -- | 前置自减运算符 | --变量名 | ||
| -- | 后置自减运算符 | 变量名-- | ||
| * | 取值运算符 | *指针变量 | ||
| & | 取地址运算符 | &变量名 | ||
| ! | 逻辑非运算符 | !表达式 | ||
| ~ | 按位取反运算符 | ~表达式 | ||
| sizeof | 长度运算符 | sizeof(表达式) | ||
| 3 | 左到右 | |||
| / | 除 | 表达式/表达式 | ||
| * | 乘 | 表达式*表达式 | ||
| % | 余数(取模) | 整形表达式/整形表达式 | ||
| 4 | 左到右 | |||
| + | 加 | 表达式+表达式 | ||
| - | 减 | 表达式-表达式 | ||
| 5 | 左到右 | |||
| << | 左移 | 变量 | ||
| >> | 右移 | 变量>>表达式 | ||
| 6 | 左到右 | |||
| > | 大于 | 表达式>>表达式 | ||
| >= | 大于等于 | 表达式>=表达式 | ||
| < | 小于 | 表达式 | ||
| <= | 小于等于 | 表达式 | ||
| 7 | 左到右 | |||
| == | 等于 | 表达式==表达式 | ||
| != | 不等于 | 表达式!=表达式 | ||
| 8 | & | 按位与 | 表达式&表达式 | 左到右 |
| 9 | ^ | 按位异或 | 表达式^表达式 | 左到右 |
| 10 | | | 按位或 | 表达式|表达式 | 左到右 |
| 11 | && | 逻辑与 | 表达式&&表达式 | 左到右 |
| 12 | || | 逻辑或 | 表达式||表达式 | 左到右 |
| 13 | 右到左 | |||
| ?: | 条件运算法 | 表达式1?表达式2:表达式3 | ||
| = | 赋值运算符 | 变量=表达式 | ||
| /= | 除后赋值 | 变量/=表达式 | ||
| *= | 乘后赋值 | 变量*=表达式 | ||
| %= | 取模后赋值 | 变量%=表达式 | ||
| 14 | 右到左 | |||
| -= | 减后赋值 | 变量-=表达式 | ||
| <<= | 左移后赋值 | 变量 | ||
| >>= | 右移后赋值 | 变量>>=表达式 | ||
| &= | 按位与后赋值 | 变量&=表达式 | ||
| ^= | 按位异或后赋值 | 变量^=表达式 | ||
| |= | 按位或后赋值 | 变量|=表达式 | ||
| 15 | , | 逗号运算符 | 表达式,表达式 | 左到右 |
注意:
- 如果不确定运算符的优先级,可以加括号
- 多用括号,让代码的可读性更好
3.8三目运算
三目运算也叫条件运算或三元运算,可以实现简单if语句的功能,但是书写更简洁
语法:表达式一?表达式二:表达式三
先计算表达式一的值,如果为真,整个表达式的结果为表达式二的值,如果为假,整个表达式的结果为表达式三的值
示例:
int a = 7, b = 5, c = 0;
//比较a和b的大小,把较大者赋值给c
c = a > b ? a : b; //a是否大于b?是,则c=a;不是,则c=b
等同于
int a=7,b=5,c=0;
if(a>b) c=a;
else c=b;
注意:三目运算表达式可以嵌套使用,但,过于复杂的三目运算表达式不方便理解,此时建议使用if语句
示例:
//判断year是不是闰年,是则返回1,不是返回0
int year;
year=(year%100==0)?(year%400==0?1:0):(year%4==0?1:0)
4、if语句
4.1程序流程的结构
- 顺序结构:从头到尾一句接着一句的执行下去,直到执行完最后一句
- 选择结构:程序执行到某个节点时,根据判断条件的结果来决定执行的分支,由if和switch语句来实现
- 循环结构:程序中有一个循环体,循环体里是一段代码,程序运行的时候,通过判断条件来决定是否执行循环体,由while、do while和for三种语句来实现
4.2if语句的基本结构
语法:
if(表达式)
{
//表达式为真(true)时执行的语句
}
else
{
//表达式为假(false)时执行的语句
}
注意:
- if(表达式)之间的空格可以不写
- 表达式必须使用括号
- 表达式可以时具体的值
- 表达式一般时关系运算和逻辑运算表达式,也可以是赋值运算或其它的表达式
- 不要把if(变量名==值)写成if(变量名=值)
- 整个if语句可以没有else分支
- if或else分支花括号内的代码如果只有一行,花括号可以不写;如果一行也没有,花括号一定要写;如果有多行,花括号一定要写,如果不写,除了第一行,其他的代码将不是if语句的一部分
- if(表达式)和else后不能加分号,因为分号表示空语句
- 在C++中,把多行语句写在大括号中,表示它们是一个语句块,不影响语句的功能
4.3嵌套使用if语句
if语句可以嵌套使用,实现多级(层)的条件判断(最多127层)
语句:
if(表达式一)
{
if(表达式二)
{
//表达式二为真(true)时执行的语句
}
else
{
//表达式二为假(false)时执行的语句
}
}
else
{
//表达式一为假(false)时执行的语句
}
4.4多条件的if语句
语法:
if(表达式一)
{
//表达式一为真(true)时执行的语句
}
else if(表达式二)
{
//表达式二为真(true)时执行的语句
}
......
else if(表达式n)
{
//表达式n为真(true)时执行的语句
}
else
{
//全部表达式都不为真时执行的语句
}
注意:
- 多条件的if语句本质上是嵌套的if语句
- 最多只能有127个条件分支
示例:
#include <iostream>
using namespace std;
int main()
{
//超女选秀:1)性别(X-女,Y-男);2)颜值(1-漂亮,0-不漂亮);3)身材(1-火辣,2-丰满,3-苗条)
// 声明三个变量,存放超女的性别、颜值和身材数据
char sex; //性别(X - 女,Y - 男)
bool yz; //颜值(1-漂亮,0-不漂亮)
int sc; //身材(1 - 火辣,2 - 丰满,3 - 苗条)
// 显示“请输入超女的性别(X-女,Y-男):”的提示文字
cout << "请输入超女的性别(X-女,Y-男):";
// 输入超女的性别,存在变量中
cin >> sex;
// 判断超女的性别,如果是女,流程继续,否则程序结束
if (sex=='X')
{
// 显示“请输入超女的颜值(1-漂亮,0-不漂亮):”提示文字
cout << "请输入超女的颜值(1-漂亮,0-不漂亮):";
// 输入超女的颜值,存放在变量中
cin >> yz;
// 判断超女的颜值,如果是漂亮,流程继续,否则程序结束
if (yz==1)
{
// 显示“请输入超女的身材(1-火辣,2-丰满,3-苗条):”提示文字
cout << "请输入超女的身材(1-火辣,2-丰满,3-苗条):";
// 输入超女的身材,存放在变量中
cin >> sc;
//判断超女的身材,如果是“火辣”,显示“晋级成功”,如果是丰满,显示“待定”,程序结束
if (sc == 1) cout << "恭喜,晋级成功!";
else cout << "待定!";
}else cout << "很遗憾!";
}else cout << "不是女的!";
return 0;
}
4.5if语句中的逻辑表达式
采用括号、对齐、空格、换行有助于更清晰的表达复杂的逻辑表达式
示例:
int main()
{
//已准备好的超女数据
int age = 28; //年龄
int height = 170; //身高
string sc = "火辣"; //身材
string yz = "漂亮"; //颜值
if ((age > 20 && age < 30 ) &&
(height > 165 && height < 175) &&
(sc == "火辣" ) &&
(yz == "漂亮" || yz == "一般" ))
{
cout << "晋级成功!\n";
}
}
5、循环语句
5.1switch语句
switch也是一种选择结构的语句,可以代替简单的多条件的if语句
语法:
switch(表达式)
{
case 值一:
语句一;
break;
case 值二:
语句二;
break;
......
case 值n:
语句n;
break;
default:
上述条件都不满足时执行的语句;
}
注意:
- case后面必须是整数和字符,或者是结果为整数和字符的表达式,但不能使用变量
- default不是必须的,当没有default时,如果全部的case匹配失败,那么就什么都不执行
- 每个分支不要漏写break语句
5.2while循环语句
语法:
while(表达式)
{
语句块;
}
先计算表达式的值,如果值为真就执行语句块,执行完语句块后,回到循环首部再次计算表达式的值,如果为真又执行一次语句块......,这个过程会一直重复,直到表达式的值为假时不再执行语句块
注意:
- 如果表达式的值永远为真,那么将进入死循环,所以在循环中应有改变表达式的值的方法
- 如果循环体中的语句块只有一行代码,大括号可以不写
- 语句块,是指在C++中,用大括号包含的一段代码
示例:
//有十个超女,编号是1-10,在控制台输出这十个超女的编号
int no = 1; //超女的编号初始化
while (no <= 100)
{
cout << "这是第" << no << "名超女的编号\n";
no++; //此段代码可写到上一句的“no”上————变成“no++”
}
5.3循环的跳转
- break和continue两个关键字用于控制循环体中代码的执行流程
- break跳出(中止)当前循环语句
- continue回到当前循环语句的首部
示例:
#include<iostream>
using namespace std;
int main()
{
//break跳出(中止)当前循环语句,continue回到当前循环语句的首部
//程序运行后一直工作,逐个输入超女的数据,判断是否晋级,如果到了休息时间,就把程序停下来
//超女选秀的流程:漂亮,直接晋级;不漂亮,身材火辣的也晋级
bool once = true; //是否为第一次执行循环
while (true)
{
if (once == false)
{
//显示“是否继续下一名超女选秀(1-继续,0-结束):”的提示文字
cout << "是否继续下一名超女选秀(1-继续,0-结束):";
//输入是否继续的决定,存放在变量中
bool exist; cin >> exist;
//判断输入的决定,如果是结束,流程跳出循环
if (exist == false) break;
}
once = false; //表示循环已经被执行过
//显示“请输入超女的颜值(1-漂亮,0-不漂亮):”的提示文字
cout << "请输入超女的颜值(1-漂亮,0-不漂亮):";
//输入超女的颜值,存放在变量中
bool yz; cin >> yz;
//判断超女的颜值,如果漂亮,显示“晋级成功”,流程跳转到循环的首部
if (yz == true)
{
cout << "晋级成功\n"; continue;
}
}
return 0;
}
5.4for循环语句
语法:
for(语句一;表达式;语句二)
{
语句块
}
- 1)循环开始的时候,先执行语句一,在整个循环过程中语句一只会被执行一次
- 2)判断表达式的值,如果为真,就执行一次循环体中的语句块
- 3)执行完语句块后,执行一次语句二
- 4)重复第2步和第3步,直到表达式不为真时才会结束for循环
示例:
//有十个超女,编号是1-10,在控制台输出这十个编号
int no; //声明超女的编号
for (no = 1; no <= 10; no++)
{
cout << "这是第" << no << "名超女的编号。\n";
}
注意:
- for循环与while循环在本质上没有区别
- for循环一般需要一个相当于计数器的变量
- 在for循环的语句一中,可以声明计数器变量
- 在for循环中,语句一、表达式和语句二都可以为空,for(;;)等同于while(true)
- continue和break两个关键字也可以用在for循环体中
5.5程序流程的结构
- 在选择结构中还有选择结构
- 在循环结构中还有循环结构
- 选择结构和循环结构也可以互相嵌套
示例:
//九九乘法表
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= i; j++)
{
cout << j << "*" << i << "=" << j * i << " ";
}
cout << endl;
}
5.6do...while循环语句(不重要)
语法:
do
{
语句块;
}while(表达式)
功能与while语句类似,不同的是:
- 进入循环时,先执行一次语句块,再计算表达式的值
- 循环的首部书写在循环的尾部,(表达式)后面还有一个分号
示例:
//有十个超女,编号是1-10,在控制台输出这十个超女的编号
int no = 1; //超女的编号初始化
do
{
cout << "这是第" << no++ << "名超女的编号\n";
} while (no <= 10);
5.7goto 语句(不重要)
也称无条件转移语句
goto的语法:goto 语句标号;
语句标号的语法:语句标号:
- 如果在程序中使用了goto,程序的流程将跳转到语句标号的位置,并执行它后面的代码
- 其中语句标号是按标识符规定书写的符号,放在某一语句行的前面,可以独占一行,标号后加半角冒号
- 语句标号用于标识语句的位置,与goto语句配合使用
- 在实际开发中,goto语句容易造成程序流程的混乱,不方便理解,调试也更麻烦,不建议使用
示例:
cout << "这是第1名超女的编号。\n";
goto bbb;
cout << "这是第2名超女的编号。\n";
cout << "这是第3名超女的编号。\n";
cout << "这是第4名超女的编号。\n";
bbb:
cout << "这是第5名超女的编号。\n";
6、函数
6.1函数的声明和定义
在复杂的程序中,如果全部的代码都写在main函数中,main函数体将非常庞大臃肿
把任务分工到其他的函数中,main函数只负责程序的核心流程,具体的任务有其他函数完成,这种思想就是模块化编程
声明和定义函数的语法:
返回值的数据类型 函数名(参数一的数据类型 参数一,参数二的数据类型 参数二,......)
{
实现函数功能得代码
return 返回值;
}
注意:
- 函数的声明和定义可以书写在一起,也可以分开,如果书写在一起,一般放在main函数得上面,如果分开,一般在main函数得上面声明,在main函数的下面定义
- 如果函数的声明和定义分开书写,函数的声明后面一定要有分号,函数的定义后面一定不能写分号
- 在同一个程序中,函数只需要声明和定义一次,也可以多次声明,但只能定义一次
- 函数的声明必须和函数的定义一致(返回值的数据类型、函数名、参数列表)
- return语句返回值的数据必须与函数的声明一致
- 如果函数的重点是实现功能,不关心返回值,返回值的数据类型填void,return语句后面就空着
- 函数可以没有任何参数
示例:
//写一个函数,给它两个整数,让它比较两个整数的大小
int max(int a, int b);
int max(int a, int b)
{
int c;
if (a > b)c = a;
else c = b;
return c;
}
int main()
{
}
6.2函数的调用
语法:函数名(参数一,参数二,......)
注意:
- 声明函数的代码必须放在调用之前,定义函数的代码可以放在调用之后
- 调用函数的时候,参数列表必须与函数的声明一致(参数的个数,书写的顺序和数据类型)
- 不管在什么地方,都不能调用main函数,但是,在普通函数中可以调用main函数外的其它普通函数
- 调用函数的代码可以独占一条语句,也可以用于表达式(赋值运算,算术运算,关系运算,函数的参数)
- 如果函数用于表达式中,返回值的数据类型要匹配(否则可能会被隐式转换或编译错误)
- 如果函数有返回值,可以不关心它,忽略它
6.4变量的作用域
是指程序中变量存在(或生效)的区域,超过该区域变量就不能被访问
变量分全局变量和局部变量,全局变量在整个程序中都可以访问,局部变量只能在函数或语句块的内部才能访问
C++中定义变量的场景主要有五种;
- 在全部函数外部定义的是全局变量
- 在头文件中定义的是全局变量
- 在函数和语句块内部定义的是局部变量
- 函数的参数是该函数的局部变量
- 函数内部用static修饰的是静态局部变量
示例:
int aa;
void func1();
void func2();
int main()
{
aa = 10;
func1();
func2();
cout << "main aa=" << aa << endl;
}
void func1()
{
cout << "func1 aa=" << aa++ << endl;
}
void func2()
{
cout << "func2 aa=" << aa++ << endl;
1)全局变量
- 在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问
- 全局变量在主程序退出时由系统收回内存空间
2)局部变量
- 在函数或语句块内部的语句使用,在函数或语句块外部是不可用的
- 局部变量在函数返回或语句块结束时由系统收回内存空间
3)静态局部变量
- 用static修饰的静态局部变量只执行初始化一次,知道程序运行结束后才释放
- 用static修饰的局部变量生命周期和程序相同,并且只会被初始化一次
- 其作用域为局部,当定义它的函数或语句块结束时,其作用域随之结束
- 当程序想要使用全局变量的时候应该优先考虑使用static(考虑到数据安全性)
注意事项:
- 全局变量和静态局部变量自动初始化为0
- 局部变量不会自动初始化,其值是不确定的,程序中应该有初始化局部变量的代码,否则编译可能会报错(不同的编译器不一样)
- 局部变量和全局变量的名称可以相同,在某函数或语句块内部,如果局部变量名与全局变量名相同,就会屏蔽全局变量,如果想使用全局变量,可以在变量名前加两个冒号(::)
- for循环初始化语句中定义的变量是局部变量
6.5函数参数的传递
调用函数的时候,调用者把数值赋给了函数的参数
实参:调用者程序中书写的在函数名括号中的参数,可以是常量、变量和表达式
形参:函数的参数列表
示例:
#include<iostream>
using namespace std;
void func(int no, string str); //向超女表白
int main()
{
int bh = 3;
string message = "我是一只傻鸟";
func(bh, message);
cout << "亲爱的" << bh << "号:" << message << endl;
}
void func(int no, string str)
{
no = 5; str = "我是一只沙雕";
cout << "亲爱的" << no << "号:" << str << endl;
}
6.6函数分文件编写
- 头文件(*.h):需要包含的头文件,指定命名空间,声明全局变量,函数的声明,数据结构和类的声明等
- 源文件(*.cpp):函数的定义、类的定义
- 主程序:main函数,程序的核心流程,需要用#include“头文件名”把头文件包含起来
编译:
Windows是集成开发环境,不需要写编译指令
在Linux系统下,把全部的源文件一起编译,如:g++ -o demo demo.cpp public.cpp
示例:
demo01.cpp
#include"tools.h"
#include"girls.h"
int main()
{
cout << "max(5,8)=" << max(5,8)<< endl;
cout << "min(5,8)=" << min(5, 8) << endl;
print(3, "我是一只傻鸟");
}
tools.h
#pragma once
#include<iostream>
using namespace std;
int max(int a, int b); //比较两个数的大小,返回较大值
int min(int a, int b); //比较两个数的大小,返回较小值
girls.h
#pragma once
#include<iostream>
using namespace std;
void print(int no, string str); //表达神器
tools,cpp
#include"tools.h"
int max(int a, int b)
{
return a > b ? a : b;
}
int min(int a, int b)
{
return a < b ? a : b;
}
girls.cpp
#include"girls.h"
void print(int no, string str) //表白神器
{
cout << "亲爱的" << no << "号:" << str << endl;
}
注意:包含系统的头文件用<>,包含自定义的头文件用" "。
6.7递归函数
递归的意思是调用自己
示例:
#include<iostream>
using namespace std;
int f(int x) //递归函数
{
if (x == 0)return 0; //递归终止的条件
return x + f(x - 1); //在函数体中调用了自己
}
int main()
{
cout << "f(3)=" << f(3) << endl;
//100+99+98+...+1+0 嵌套的调用函数 进入函数的过程是递去 函数返回的过程是归来
//计算从1加到100的和
int sum = 0; //存放累加的值
for (int i = 1; i <= 100; i++)
sum = sum + i;
cout << "sum=" << sum << endl;
}
7、VS中调试程序
-
F9设置/取消断点
-
F5/F10开始调试
-
Shift+F5放弃调试
-
F10逐过程执行
-
F11逐语句执行(可进入函数内部)
局部变量窗口显示了变量的值,也可以修改
8、指针
8.1基本概念
1)变量的地址
变量是内存变量的简称,在C++中,每定义一个变量
,系统就会给变量分配一块内存,内存是有地址的
C++用运算符&获取变量在内存中的起始地址
语法:&变量名
2)指针变量
指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中的起始地址
语法:*数据类型 变量名;
数据类型必须是合法的C++数据类型(int、char、double或其它自定义的数据类型)
星号*与乘法中使用的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针
3)对指针赋值
不管是整型、浮点型、字符型,还是其它的数据类型的变量,它的地址都是一个十六进制数,我们用:
整型指针存放整数型变量的地址;
字符型指针存放字符型变量的地址;
浮点型指针存放浮点型变量的地址;
自定义数据类型指针存放自定义数据类型变量的地址
语法:指针=&变量名
注意:
- 对指针的赋值操作也通俗的被称为“指向某变量”,被指向的变量的数据类型称为“基类型”
- 如果指针的数据类型与基类型不符,编译会出现警告,但是,可以强制转换他们的类型
4)指针占用的内存
指针也是变量,是变量就要占用内存空间
在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节
在C++中,指针是复合数据类型,复合数据类型是指基于其他类型而定义的数据类型,在程序中,int是整型类型,int是整型指针类型,int可以用于声明变量。可以用于sizeof运算符,可以用于数据类型的强制换行,总的来说,把int*当成一种数据类型就是了
示例:
#include<iostream>
using namespace std;
int main()
{
int a;
char b;
bool c;
string d;
int* pa = &a;
char* pb = &b;
bool* pc = &c;
string* pd = &d;
cout << "a的地址是:" << (long long)&a<<endl;
cout << "b的地址是:" << (long long)&b<<endl;
cout << "c的地址是:" << (long long)&c<<endl;
cout << "d的地址是:" << (long long)&d<<endl;
cout << endl;
cout << "a的地址是:" << (long long)pa<<endl;
cout << "b的地址是:" << (long long)pb<<endl;
cout << "c的地址是:" << (long long)pc<<endl;
cout << "d的地址是:" << (long long)pd<<endl;
}
8.2使用指针
声明指针变量后,在没有赋值之前,里面是乱七八糟的的值,这时候不能使用指针
指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量一样)
运算符被称为间接值或解除引用(解引用)运算符,将它用于指针,可以得到该地址的内存中存储的值,“”也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用
变量和指向变量的指针就像同一枚硬币的两面
程序在存储数据的时候,必须跟踪三种基本属性:
- 数据存储在哪里;
- 数据是什么类型;
- 数据的值是多少
用两种策略可以达到以上目的:
- 声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元
- 声明一个指针变量,存储的值是地址,而不是值本身,程序直接访问该内存单元
示例:
#include<iostream>
using namespace std;
int main()
{
int a = 3;
int* p = &a;
int* p1 = &a;
cout << "a=" << a << endl;
cout << "*p=" << *p << endl;
cout << "*p1=" << *p1 << endl;
//*p=8
a = 8;
cout << "a=" << a << endl;
cout << "*p=" << *p << endl;
cout << "*p1=" << *p1 << endl;
cout << "&a=" << &a << ",p1=" << p1 << ",p=" << p << endl;
}
8.3指针用于函数的参数
如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,之种被通俗的称为地址转递或传地址
值传递:函数的形参是普通变量
转地址的意义如下:
- 可以在函数中修改实参的值
- 减少内存拷贝,提升性能
示例:
#include<iostream>
using namespace std;
//写一个函数,从3名超女的身高数据中,选出最高和最矮的
void func1(int a, int b, int c, int* max, int* min) //用void说明不需要返回值
{
*max = a > b ? a : b; //取a和b中的大者
*min = a < b ? a : b; //取a和b中的小者
*max = *max > c ? *max : c; //取*max和c中的大者
* min = *min < c ? *min : c; //取*min和c中的小者
}
int main()
{
int a = 180, b = 170, c = 175, m, n;
func1(a, b, c, &m, &n);
cout << "m=" << m << ",n=" << n << endl;
int bh = 3;
}
8.4用const修饰指针
1)常量指针
语法:*const 数据类型 变量名;
不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)
注意:
- 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)
- 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值
- 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义
- 如果形参的值不需要改变,建议加上const修饰,程序可读性更好
示例:
int main()
{
int a = 3, b = 8;
const int* p = &a;
a = 13;
cout << "a=" << a << ",*p=" << *p << endl;
p = &b;
cout << "b=" << b << ",*p=" << *p << endl;
}
2)指针常量
语法:*数据类型 const 变量名;
指向的变量(对象)不可改变
注意:
- 在定义的同时必须初始化,否则没有意义
- 可以通过解引用的方法修改内存地址中的值
- C++编译器把指针常量做了一些特别的处理,改头换面之后,叫引用
示例:
int main()
{
int a = 3, b = 8;
int* const p = &a;
*p = 13;
cout << "a=" << a << ",*p=" << *p << endl;
}
3)常指针常量
语法:*const 数据类型 const 变量名;
指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值
常引用
常量指针:指针指向可以改,指针指向的值不可以更改
指针常量:指针指向不可以改,指针指向的值可以更改
常指针常量:指针指向不可以改,指针指向的值不可以更改
记忆秘诀:*表示指针,指针在前先读指针;指针在前指针就不允许改变
常量指针:*const 数据类型 变量名
指针常量:*数据类型 const 变量名
8.5void关键字
在C++中,void表示为无类型,主要有三个用途:
- 函数的返回值用void,表示函数没有返回值
void func(int a,int b)
{
//函数体代码
return;
}
- 函数的参数填void,表示函数不需要参数(或者让参数列表空着)
int func(void)
{
//函数代码
return;
}
- 函数的形参用void*,表示接受任意数据类型的指针
注意:
- 不能用void声明变量,它不能代表一个真实的变量
- 不能对void*指针直接解引用(需要转换成其它类型的指针)
- 把其它类型的指针赋值给void*指针不需要转换
- 把void*指针赋值给把其它类型的指针需要转换
示例:
//只关心地址本身,不关心里面的内容,用void*可以存放任意类型的地址
//显示变量的十六进制地址的函数:varname-变量名,p-变量的地址
void func(string varname, void* p)
{
cout << varname << "的地址是:" << p << endl;
cout << varname << "的地址值:" << *(char*)p << endl; //先把p转换为字符型指针,再解引用
}
int main()
{
//显示变量的十六进制地址
int a ;
char b ;
cout << "a的地址是:" << &a << endl;
cout << "b的地址是:" << &b << endl;
}
8.6C++内存模型
在C++中,程序运行时,内存主要分成四个区,分别是栈、堆、数据段、代码段
栈:存储局部变量、函数参数和返回值
堆:存储动态开辟内存的变量
数据段:存储全局变量和静态变量
代码段:存储可执行程序的代码和常量(例如字符常量),此存储区不可修改
栈和堆的主要区别:
1)管理方式不同:栈是编译器自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程序中不释放,程序结束时由操作系统回收
2)空间大小不同:堆内存的大小受限与物理内存空间;而栈就小得可怜,一般只有8M(可以修改系统参数)
3)分配方式不同:堆是动态分配;栈有静态分配和动态分配(都是自动释放)
4)分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由C++函数库提供的
5)是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片,太多的碎片会导致性能的下降
6)增长方式不同:栈向下增长,以降序分配内存地址,堆向上增长,以升序分配内存地址
8.7动态分配内存new和delete
使用堆区内存的有四个步骤:
1)声明一个指针
2)用new运算符向系统申请一块内存,让指针指向这块内存
3)通过对指针解引用的方法,像使用变量一样使用这块内存
4)如果这块内存不用了,用delete运算符释放它
申请内存的语法:new 数据类型(初始值); //C++11支持{}
如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)
释放内存的语法:delete 地址;
示例:
int main()
{
//1)声明一个指针
int* p = new int(5);
//2)用new运算符向系统申请一块内存,让指针指向这块内存
cout << "*p=" << *p<< endl;
//3)通过对指针解引用的方法,像使用变量一样使用这块内存
*p = 8;
//4)如果这块内存不用了,用delete运算符释放它
cout << "*p=" << *p << endl;
delete p;
}
注意:
- 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据
- 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存
示例:(警告,不能运行,不然会导致系统崩溃,运行时可打开任务管理器即可查看内存状态)
for(int a=1;a>0;a++)
{
int*p=new int[10000]; //一次申请10000个整数
cout<<"a="<<a<<",p="<<p<<endl;
}
- 动态分配的内存生命周期和程序相同,程序退出时,如果没有释放,系统将自动回收
- 用指针跟踪已分配的内存时,不能跟丢
- 就算指针的作用域已失效,所指向的内存也不会释放
8.7二级指针
指针是指指针变量的简称,也是变量,是变量就有地址
指针用于存放普通变量的地址
二级指针用于存放指针变量的地址
声明二级指针的语法:**数据类型**** 指针名;
使用指针有两个目的:1)传递地址 2)存放动态分配的内存的地址
在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针
把普通变量的地址传入函数后可以在函数中修改变量的值;把指针的地址传入函数后可以在函数中指针的值
示例:
int main()
{
int i = 8; cout << "i=" << i << ",i的地址是:" << &i << endl;
int* pi = &i; cout << "pi=" << pi << ",pi的地址是:" << &pi << ",*pi=" << *pi << endl;
int** ppi = π cout << "ppi=" << ppi << ",ppi的地址是:" << &ppi <<",*ppi="<<*ppi<< endl;
cout << "**ppi=" << **ppi << endl;
}
示例:
void func(int **pp)
{
*pp = new int(3);
cout << "pp=" << pp << ",*pp=" << *pp << endl;
}
int main()
{
int* p = 0;
func(&p);
cout << "p=" << p << ",*p=" << *p << endl;
}
8.8空指针
在C和C++中,用0或NULL都可以表示空指针
声明指针后,在赋值之前,让它指向空,表示没有指向任何地址
1)使用空指针的后果
如果对空指针解引用,程序会奔溃
示例:
int*p=0;
cout<<"p="<<p<<",*p="<<*p<<endl;//结果显示代码为负数,表示异常退出既程序崩溃
如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常所以,内存释放后,也应该把指针指向空
示例:
int*p=0;
delete p;cout<<"delete ok.\n";
在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性
为啥空指针访问会出现异常?
NULL指针分配的分区:其范围是从0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区
2)C++11的nullptr
用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是(void*)0.
NULL在C++中就是0,这是因为在C++中void*类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整型的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议用nullptr替代NULL吧,而NULL就当做0使用
注意:在Linux平台下,如果使用nullptr。编译需要加-std=c++11参数
8.9野指针
野指针就是指针指向的不是一个有效(合法)的地址
在程序中,如果访问野指针,可能会造成程序的奔溃
示例:
int*p=0x00999333333; //这是一个无效的地址
cout<<"p="<<p<<",*p="<<*p<<endl;
出现野指针的情况主要有三种:
-
指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)
-
如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效
-
指针指向的变量一超越变量作用域(变量的内存空间已被系统回收)
规避方法:
- 指针在定义的时候,如果没地方指。就初始化为nullptr
- 动态分配的内存被释放后,将其置为nullptr
- 函数不要返回局部变量的地址
注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序的崩溃,但也不一定。程序的表现是不稳定,增加了调试的难度
8.10函数指针
函数的二进制代码存放在内存四区中的代码段,函数的地址它在内存中的开始地址。如果把函数的地址作为参数,就可以在函数中灵活的调用其他的函数
使用函数指针的三个步骤:
- 声明函数指针
- 让函数指针指向函数的地址
- 通过函数指针调用函数
1)声明函数指针
声明普通指针时,必须提供指针的类型。同样,声明函数指针时,也必须提供函数类型,函数的类型是指返回值和参数列表(函数名和形参名不是)
假设函数的原型是:
int func1(int bh,string str);
int func2(int no,string message);
int func3(int id,string info);
bool func4(int id ,string info);
bool func5(int id);
则函数指针的声明是:
int (*pfa)(int,string);
bool(*pfb)(int,string);
bool(*pfc)(int);
示例:(指针语法)
void func(int no,string str)
{
cout << "亲爱的" << no << "号" << str<< endl;
}
int main()
{
int bh = 3; //超女的编号
string message = "我是一只傻鸟"; //向超女表白的内容
func(bh, message);
void(*pfunc)(int, string); //声明表白函数的函数指针
pfunc = func; //对函数指针赋值,语法是 函数指针名=函数名
pfunc(bh, message); //用函数指针名调用函数。C++
(*pfunc)(bh, message); //用函数指针名调用函数。c语言
}
9、数组
9.1一维数组的基本概念
数组是一组数据类型相同的变量,可以存放一组数据
1)创建数组
声明数组的语法:数据类型 数组名[数组长度];
注意:
- 数组长度必须是整数,可以是常量,也可以是变量和表达式
- C90规定必须用常量表达式指明数组的大小,C90允许使用整型非常量表达式。经测试,在VS中可以用整型给常量表达式,不能用变量;但是,Linux中还可以用变量
2)数组的使用
可以通过下标访问数组中元素,数组下标从0开始
数组中每个元素的特征和使用方法与单个变量完全相同
语法:数组名[数组下标]
注意:
- 数组下标也必须是整数,可以是常量,也可以是变量
- 合法的数组下标取值是:0~(数组长度-1)
3)数组占用内存的情况
数组在内存中占用的空间是连续的
用sizeof(数组名)可以得到整个数组占用内存空间的大小(只适用于C++基本数据类型)
示例:
int main()
{
int bh[3]; //超女编号
string name[3]; //超女姓名
cout << "数组bh占用的内存空间是:" << sizeof(bh) << endl;
//可以通过下标访问数组中元素,数组下标从0开始
//数组中每个元素的特征和使用方法与单个变量完全相同
for (int i = 0; i < 3; i++)
{
bh[i] = 30 + i;
}
for (int i = 0; i <3; i++)
{
cout << "bh[" << i << "]=" << bh[i] << endl;
}
}
4)数组的初始化
声明的时候初始化:
数据类型 数组名[数组长度]={值1,值2,...};
数据类型 数组名[ ]={值1,值2,...};
数据类型 数组名[数组长度]={0}; //把全部的元素初始化为0
数组类型 数组名[数组长度]={};//把全部的元素初始化为0
注意:
- 如果{}内不足数组长度个数据,剩余数据用0补全,但是,不建议这么用,你可能在数组中漏了某个值。如果想把数组中全部的元素初始化为0,可以在{}内只填一个0或什么也不填
- C++11标准可以不写等于号
5)清空数组
用memset()函数可以把数组中全部的元素清零。(只适用于C++基本数据类型)
函数原型:**void memset(void s,int c,size_t n);
注意,在Linux下,使用memcpy()函数需要包含头文件#include<string.h>
6)复制数组
用memcpy()函数可以把数组中全部的元素复制到另一个相同大小的数组。(只适用于C++基本数据类型)
函数原型:**void *memcpy(void dest,const void src,size_t );
注意,在Linux下,使用memcpy()函数需要包含头文件#include<string.h>
9.2一维数组和指针
1)指针的算术
将一个整型变量加1后,其值将增加1
但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数
示例:
int main()
{
char a; cout << "sizeof(char)=" << sizeof(char) << endl; //1字节
short b; cout << "sizeof(short)=" << sizeof(short) << endl; //2字节
int c; cout << "sizeof(int)=" << sizeof(int) << endl; //4字节
double d; cout << "sizeof(double)=" << sizeof(double) << endl; //8字节
cout << "a的地址是:" << (void*) & a << endl;
cout << "a的地址+1是:" << (void*)( & a + 1) << endl;
cout << "b的地址是:" << (void*)&b << endl;
cout << "b的地址+1是:" << (void*)(& b + 1) << endl;
cout << "c的地址是:" << (void*)&c << endl;
cout << "c的地址+1是:" << (void*)(& c + 1) << endl;
cout << "d的地址是:" << (void*)&d << endl;
cout << "d的地址+1是:" << (void*)(& d + 1) << endl;
}
2)数组的地址
- 数组在内存中占用的空间是连续的
- C++将数组名解释为数组第0个元素的地址
- 数组第0个元素的地址和数组首地址的取值是相同的
- 数组第n个元素的地址是:数组首地址+n
- C++编译器把 数组名[下标] 解释为*(数组首地址+下标)
3)数组的本质
数组是占用连续空间的一块内存,数组名解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的
数组本质是在内存中读取一段地址,数组名是地址的首地址。指针同样是指向地址,就是指向数组的首地址
4)数组名不一定会被解释为地址
在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数
可以修改指针的值,但数组名是常量,不可修改
9.3一维数组的越界
示例:
int main()
{
int a[5] = { 3,6,5,8,9 };
//用数组表示法操作数组
for (int i = 0; i < 5; i++)
{
cout << "a[" << i << "]的值是:" << a[i] << endl;
}
//用指针表示法操作数组
int* p = a;
for (int i = 0; i < 5; i++)
{
cout << "*(p+" << i << ")的值是:" << *(p + i) << endl;
}
}
- 在for循环中,循环的条件超过了循环数(上例中的5),就是数组的越界
- 数组越界会使系统崩溃或者数值混乱
- 数组越界的本质是野指针,不管采用数组表示法还是指针表示法都是野指针
规避方法:
- 写程序时小心点
- 用C++标准库封装好的数组,就肯定不会出现越界的情况
9.4一维数组用于函数的参数
1)指针的数组表示
在C++内部,用指针来处理数组
C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)
C++编译器把 地址[下标] 解释为 *(地址+下标)
2)一维数组用于函数的参数
一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志
书写方法有两种:
void func(int*arr,int len);
void func(int arr[],int len);
注意:
- 在函数中,可以用数组表示法,也可以用指针表示法
- 在函数中,不要对指针名用sizeof运算符,它不是数组名
- C++中,int*arr替换了int arr[]
示例:
void func(int arr[], int len)
{
for (int i = 0; i < len; i++)
{
cout << "arr[" << i << "]的值是:" << arr[i] << endl; //数组名[下标] 解释为 *(数组首地址+下标)
cout << "*(arr+" << i << ")的值是:" << *(arr + i) << endl; //地址[下标] 解释为 *(地址+下标)
}
}
int main()
{
int a[5] = { 3,6,5,8,9 };
func(a, sizeof(a) / sizeof(int));
////用数组表示法操作数组
//for (int i = 0; i < 5; i++)
//{
// cout << "a[" << i << "]的值是:" << a[i] << endl;
//}
////用指针表示法操作数组
//int* p = a;
//for (int i = 0; i < 5; i++)
//{
// cout << "*(p+" << i << ")的值是:" << *(p + i) << endl;
// cout << "p[" << i << "]的值是:" << p[i] << endl;
//}
}
9.5用new动态创建一维数组
普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存
动态创建一维数组的语法:*数据类型 指针=new 数据类型[数组长度];
释放一维数组的语法:delete[] 指针;
示例:
int main()
{
int *arr = new int[8]; //创建8个元素的整型数组
for (int i = 0; i < 8; i++)
{
arr[i] = 100 + i; //数组表示法
cout << "arr[" << i << "]=" << *(arr + i) << endl; //指针表示法
}
delete[]arr;
}
注意:
- 动态创建的数组没有数组名,不能用sizeof运算符
- 可以用数组表示法和指针表示法两种方式使用动态创建的数组
- 必须用delete[]来释放内存(不能只用delete)
- 不要用delete[]来释放不是new[]分配的内存
- 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)
- 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)
- 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放
- 如果内存不足,调用new会产生异常,导致程序中;如果在new关键字后面加(std::nothrow)选项。则返回nullptr,不会产生异常
- 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配的内存
示例:
int main()
{
int* a = new(std::nothrow)int[1000000000001];
if (a == nullptr)
{
cout << "分配内存失败。\n";
}
else
{
a[1000000000000] = 8;
delete[] a;
}
}
9.6一维数组的排序qsort
qsort()函数用于对各种数据类型的数组进行排序
函数的原型:
void qsort(void * base,size_t nmemb,size_t size, int( compar)(const void*,const void ));
第一个参数:数组的起始地址
第二个参数:数组元素的个数(数组长度)
第三个参数:数组元素的大小(sizeof(数组的数据类型))
第四个参数:回调函数的地址
回调函数决定了排序的顺序,声明如下:
*int compar(const void * p1,const void p2);
- 如果函数的返回值<0,那么p1所指向元素会被排在p2所指向元素的前面
- 如果函数的返回值==0,那么p1所指向元素与p2所指向元素的顺序不确定
- 如果函数的返回值>0,那么p1所指向元素会被排在p2所指向元素的后面
示例:
int compasc(const void* p1, const void* p2) //升序的回调函数
{
return*((int*)p1) - *((int*)p2);
}
int compdesc(const void* p1, const void* p2) //降序的回调函数
{
return*((int*)p2) - *((int*)p1);
}
int main()
{
int a[8] = { 4,2,7,5,8,6,1,3 };
//函数的原型
qsort(a, sizeof(a) / sizeof(int), sizeof(int), compasc); //对数组a进行升序排序
for (int i = 0; i < 8; i++)
{
cout << "a[" << i << "]=" << a[i] << endl;
}
qsort(a, sizeof(a) / sizeof(int), sizeof(int), compdesc); //对数组a进行降序排序
for (int i = 0; i < 8; i++)
{
cout << "a[" << i << "]=" << a[i] << endl;
}
}
qsort()函数的其它细节:
- 形参中的地址用void是为了支持任意数据类型,在回调函数中必须具体化
- 为啥需要第三个形参size_t size?因为在函数的内部,操作数据的时候不是按数据类型来操作的,而是按内存块来操作
- size_t是C标准库中定义的,在64位系统中是8字节无符号整型(unsigned long long)typedef unsigned long long size_t
- 排序的需求除了升序和降序,还有很多不可预知的情况,只能用回调函数
9.7一维数组的排序和查找-折半查找(二分查找算法)
如何快速猜出商品的价格
示例:
//在arr中查找key,成功返回key在arr中的数组下标,失败返回-1
int search(int arr[], int len, int key)
{
int low = 0, high = len - 1, mid; //初始化:low=0,high=数组长度-1
while (low <= high)
{
mid = (low + high) / 2; //计算mid指针的位置
if (arr[mid] == key)return mid; //查找成功
else if (arr[mid] > key)high = mid - 1; //继续在前半区查找
else low = mid + 1; //继续在后半区查找
}
return -1; //查找失败
}
int main()
{
int a[10] = { 7,9,12,16,21,25,30,35,41,48 }; //必须是已排好序的数组
if (search(a, 10, 30) >= 0)cout << "在数组a中查找30成功。\n";
else cout << "在数组a中查找30失败。\n";
}
9.8C风格字符串
- string使用方便,能自动扩展,不用担心内存问题
- string是C++的类,封装了C风格的字符串
- 在某些场景中,C风格字符串更方便,更高效
- C标准库、Linux系统、开源库和数据库的接口函数
C语言约定:如果字符型(char数组)的末尾包含了空字符\0(也就是0),那么该数组中的内容就是一个字符串
因为字符串需要用0结尾,所以在声明字符数组的时候,要预留一个字节用来存放0
char name[21]; //声明一个最多存放20个英文字符或十个中文的字符串
1)初始化方法
char name[11]; //可以存放10个字符,没有初始化,里面是垃圾值
char name[11]="hello"; //初始内容为hello
char name[]={"hello"}; //初始内容为hello,系统会自动添加\0,数组长度是6
char name[11]={"hello"}; //初始内容为hello
char name[11] {"hello"}; //初始内容为hello
char name[11]={0]; //把全部的元素初始化为\0
2)清空字符串
memset(name,0,sizeof(name)); //把全部的元素置为0
name[0]=0; //不规范,有隐患,不推荐
3)字符串复制或赋值strcpy()
*char strcpy(char * dest,const char * src);
功能:将参数src字符串拷贝至参数dest所指的地址
返回值:返回参数dest的字符串起始地址
注意:使用strcpy()函数时,文件上面要添加#define_CRT_SECURE_NO_WARNINGS
复制完字符串后,会在dest后追加0
如果参数dest所指的内存空间不够大,会导致数组的越界
4)字符串复制或赋值strncpy
char * strncpy(char * dest,const char * src,const size_t n);
功能:把src前n个字符的内容复制到dest中
返回值:dest字符串起始地址
如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个
如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0
如果参数dest所指的内存空间不够大,会导致数组的越界
5)获取字符串的长度strlen()
size_t strlen(const char * str);
功能:计算字符串的有效长度,不包含0
返回值:返回字符串的字符数
strlen()函数计算的是字符串的实际长度,遇到0结束
6)字符串拼接strcat()
char strcat(char * dest,const char src);
功能:将src字符串拼接到dest所指的字符串尾部
返回值:返回dest字符串起始地址
dest最后原有的结尾字符0会被覆盖,并在链接后的字符串的尾部再增加一个0
如果参数dest所指的内存空间不够大,会导致数组的越界
7)字符串拼接strncat()
*char strncat(char * dest,const char * src,const size_t n);
功能:将src字符串的前n个字符拼接到dest所指的字符串尾部
返回值:返回dest字符串的起始地址
如果n大于等于字符串src的长度,那么将src全部追加到dest的尾部,如果n小于字符串src的长度,只追加src的前n个字符
strncat会将dest字符串最后的0覆盖掉,字符追加完成后,再追加0
如果参数dest所指的内存空间不够大,会导致数组的越界
8)字符串比较strcmp()和strncmp()
**int strcmp(const char str1,const char str2);
功能:比较str1和str2的大小
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1
**int strncmp(const char str1,const char str2,const size_t n);
功能:比较str1和str2前n个字符的大小
返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;
两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等
9)字符查找strchr()和strrchr()
**char strchr(const char s,const int c);
返回一个指向在字符串s中第一个出现c的位置,如果找不到,返回0
10)字符串查找strstr()
*char strstr(const char * str , const char * substr);
功能:检索子串在字符串中首次出现的位置
返回值:返回字符串s中第一次出现子串substr的地址,如果没有检索到子串,返回0
11)用于string的表达式
可以把C风格的字符串用于包含了string类型的赋值拼接等表达式中
12)注意事项
- 字符串的结尾标志是0,按照约定,在处理字符串的时候,会从起始位置开始搜索0,一直找下去,找到为止(不会判断数组是否越界)
- 结尾标志0后面的都是垃圾内容
- 字符串在每次使用前都要初始化,减少入坑的可能,是每次,不是第一次
- 不要在子函数中对字符指针用sizeof运算,所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中
- 在VS中,如果要使用C标准的字符串操作函数,要在源代码文件的最上面加#define_CRT_SECURE_NO_WARNINGS strcpy_s strcat_s
9.9二维数组
一维数组的数学概念是线性表,二维数组的数学概念是矩阵
1)创建二维数组
声明二维数组的语法:数据类型 数组名[行数] [列数];
注意:数组长度必须是整数,可以是常量,也可以是变量和表达式
C90规定必须用常量表达式指明数组的大小,C99允许使用整型非常量表达式。经测试,在VS中可以用整型非常量表达式,不能用变量;但是,Linux中还可以用变量
2)二维数组的使用
可以通过行下表和列下标访问二位数组中元素,下标从0开始
二维数组中每个元素的特征和使用方法与单个变量完全相同
语法:数组名 [行下标] [列下标]
注意:
- 二维数组下标也必须是整数,可以是常量,也可以是变量
- 合法的行下标取值是:0~(行数-1)
- 合法的列下标取值是;0~(列数-1)
示例:
int main()
{
int bh[2][3]; //声明一个两行三列的二维数组,存放超女的编号
bh[0][0] = 11; bh[0][1] = 12; bh[0][2] = 13;
bh[1][0] = 11; bh[1][1] = 12; bh[1][2] = 13;
cout << "bh[0][0]=" << bh[0][0] << " bh[0][1]=" << bh[0][1] << " bh[0][2]=" << bh[0][2] << endl;
cout << "bh[1][0]=" << bh[1][0] << " bh[1][1]=" << bh[1][1] << " bh[1][2]=" << bh[1][2] << endl;
for (int i = 0; i < 2; i++) //第一层循环表示行数,循环继续的条件是计数器小于行数
{
for (int j = 0; j < 3; j++) //第二层循环表示列数,循环继续的条件是计数器小于列数
{
cout << "bh[" << i << "][" << j << "]=" << bh[i][j] << " "; //处理二维数组的每个元素
}
cout << endl; //每处理一行数据后,输出一个换行
}
int*p=(int*)bh;
for(int i=0;i<6;i++)
{
cout<<"p["<<i<<"]="<<p[i]<<endl;
}
}
3)二维数组占用内存的情况
用sizeof(数组名)可以得到整个二维数组占用内存空间的大小(只适用于C++基本数据类型)
二维数组在内存中占用的空间是连续的
示例:
int* p = (int*)bh;
for (int i = 0; i < 6; i++)
{
cout << "p[" << i << "]=" << p[i] << endl;
}
}
4)二维数组的初始化
声明的时候初始化:
数据类型 数组名[行数] [列数]={{数据1,数据2},{数据3,数据4},......};
数据类型 数组名[行数] [列数]={数据1,数据2, 数据3,数据4,......};
数据类型 数组名[] [列数]={数据1,数据2,数据3,数据4,......};
C++11标准可以不写等于号
5)清空二维数组
用memset()函数可以把二维数组中全部的元素清零(只适用于C++基本数据类型)
函数原型:**void memset(void s,int c,size_t n);
注意,在Linux下,使用memcpy()函数需要包含头文件#include<string.h>
memset(bh,0,sizeof())
6)复制二维数组
用memcpy()函数可以把二维数组中全部的元素复制到另一个相同大小的数组(没说多少维)
适用于C++基本数据类型
9.10二维数组用于函数的参数
int*p; //整型指针
int*p[3]; //一维整型指针数组,元素是3个整型指针(p[0]、p[1]、p[2])
int*p(); //函数p的返回值类型是整型的地址
int(*p)(int,int); //p是函数指针,函数的返回值是整型
1)行指针(数组指针)
声明行指针的语法:数据类型(*行指针名)[行的大小]; //行的大小即数组长度
int(*p1)[3]; //p1是行指针,用于指向数组长度为3的int型数组
int(*p2)[5]; //p2行指针,用于指向数组长度为5的int型数组
double(*p3)[5]; //p2是行指针。用于指向数组长度为5的double型数组
一维数组名被解释为数组第0个元素的地址
对一维数组名取地址得到的是数组的地址,是行地址
2)二维数组名是行地址
int bh[2] [3]={{11,12,13},{21,22,23}};
bh是二维数组名,该数组有2两元素,每一个元素本身又是一个数组长度为3的整型数组类型的行地址
int bh[4] [2] [3];
bh是三维数组名,该数组有4个元素,每一个元素本身又是一个2行3列的二维数组
bh被解释为2行3列的二维数组类型的二维地址
如果存放bh的值,要用2行3列的二维数组类型的行指针
int(*p)[2] [3]=bh;
3)把二维数组传递给函数
如果要把bh传给函数,函数的声明如下:
void func(int(*p)[3],int len);
void func(int p[] [3],int len);
9.11多维数组
示例:
void func(int(*p)[2][3])
{
int i = 1;
//遍历三维数组p,给它的每个元素赋值
for (int a = 0; a < 4; a++)
for (int b = 0; b < 2; b++)
for (int c = 0; c < 3; c++)
p[a][b][c] = i++;
}
int main()
{
int bh[4][2][3]; //假设有4个超女方阵,每个方阵有2行,每行有3个超女
memset(bh, 0, sizeof(bh));
func(bh);
for (int a = 0; a < 4; a++)
{
for (int b = 0; b < 2; b++)
{
for (int c = 0; c < 3; c++)
cout << bh[a][b][c] << "\t";
cout << endl; //每显示一行后,输出一个换行符
}
cout << endl << endl; //每显示一个方阵后,输出两个换行符
}
}
10结构体
10.1结构体的基本概念
结构体是用户自定义的类型,可以将多种数据的表示合并到一起,描述一个完整的对象
使用结构体有两个步骤:1)定义结构体描述(类型);2)创建结构体变量
1)定义结构体描述(类型)
定义结构体描述的语法:
struct 结构体名
{
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
......
成员n的数据类型 成员名n;
};
注意:
- 结构体名是标识符
- 结构体的成员可以是任意数据类型
- 定义结构体描述的代码可以放在程序的任何地方,一般放在main函数的上面或头文件中
- 结构体成员可以用C++的类(如string),但是不提倡
- 在C++中,结构体中可以有函数,但是不提倡
- 在C++11中,定义结构体的时候可以指定初始值
2)创建结构体变量
语法:
struct 结构体名 结构体变量名;
也可以为结构成员赋初始值
struct 结构体名 结构体变量名={成员一的值,成员二的值,......,成员n的值}
C++11可以不写等于号
如果大括号内未包含任何东西或只写一个0,全部的成员都将被设置为0
struct 结构体名 结构体变量名={0};
注意:
- 在C++中,struct关键字可以不写
- 可以在定义结构体的时候创建结构体变量
3)使用结构体
在C++程序中,用成员运算符(.)来访问结构体的每个成员。结构体中的每个成员具备普通变量的全部特征
语法:结构体变量名.结构体成员名;
示例:
int main()
{
//超女基本信息结构体st_girl,存放了超女全部的数据项
struct st_girl
{
string name = "西施"; //姓名
int age = 25; //年龄
double weight = 48.6; //体重(kg)
char sex = 'X'; //性别:X-女;Y-男
bool yz = false; //颜值:true-漂亮;false-不漂亮
};
struct st_girl stgirl; //创建结构体变量(在C语言中,必须写关键字struct,在C++中可以不写)
cout << "姓名:" << stgirl.name << ",年龄:" << stgirl.age << ",体重:" << stgirl.weight << ",性别:" << stgirl.sex << ",颜值:" << stgirl.yz << endl;
}
4)占用内存的大小
用sizeof运算符可以得到结构体占用内存的大小
注意:结构体占用内存的大小不一定等于全部成员占用内存之和
内存对齐:#pragma pack(字节数)
合理使用内存对齐规则,某些节省内存的做法可能毫无意义
5)清空结构体
创建结构体的变量如果没有初始化,成员中有垃圾值
用memset()函数可以把结构体中全部的成员清零。(只适用于C++基本数据类型)
bzero()函数也可以
6)复制结构体
- 用memcpy()函数把结构体中全部的元素复制到另一个相同类型的结构体(只适用于C++基本数据类型)
- 也可以直接用等于号(只适用于C++基本数据类型)
10.2结构体指针
结构体是一种自定义的数据类型,用结构体可以创建结构体变量
1)基本语法
在指针章节中我们已经学习过,采用不同数据类型的指针指向不同数据类型的变量的地址,这一规则也适用于结构体。如下:
struct st_girl girl; //声明结构体变量girl
*struct st_girl pst=&girl; //声明结构体指针,指向结构体变量girls
通过结构体指针访问结构体成员有两种方法:
( 指针名).成员变量名 //(*pst).name和( pst).age
或者:
*指针名->成员变量名 //pst->name和 pst->age
在第一种写法中,圆点(.)优先级高于(*),( *指针名)两边的括号不能少。如果去掉括号写成( *指针名).成员变量名,那么相当于 *(指针名.成员变量名),一样就完全不一样了
在第一种写法中,->是一个新的运算符
上面的两种写法是等效的,程序员通常采用第二种写法,更直观
注意:与数组不一样,结构体变量名没有被解释为地址
2)用于函数的参数
如果要把结构体传递给函数,实参取结构体的地址,函数的形参用结构体指针
如果不希望在函数中修改结构体的值,可以对形参加const约束
3)用于动态分配内存
用结构体指针指向动态分配的内存的地址
动态分配出来的内存应该手动释放
示例:
#include<iostream>
using namespace std;
//超女基本信息结构体st_girl,存放了超女全部的数据项
struct st_girl
{
string name[21]; //姓名
int age; //年龄
double weight ; //体重(kg)
char sex ; //性别:X-女;Y-男
bool yz; //颜值:true-漂亮;false-不漂亮
};
void func(const st_girl* pst)
{
cout << "姓名:" << pst->name << ",年龄:" << pst->age
<< ",体重:" << pst->weight << ",性别:" << pst->sex
<< ",颜值:" << pst->yz << endl;
}
int main()
{
//struct st_girl stgirl; //创建结构体变量(在C语言中,必须写关键字struct,在C++中可以不写)
st_girl* stgirl = new st_girl({ 0 });
func(stgirl);
cout << "姓名:" << stgirl->name << ",年龄:" << stgirl->age
<< ",体重:" << stgirl->weight << ",性别:" << stgirl->sex
<< ",颜值:" << stgirl->yz << endl;
delete stgirl;
}
10.3结构体数组
结构体可以被定义成数组变量,本质上与其它类型的数组变量没有区别
声明语法:struct 结构体类型 数组名[数组长度];
初始化结构体数组,要结合使用初始化数组的规则和初始化结构体的规则
struct st_girl girls[2]={{"西施",26,43.8,'X',true},{"西瓜",25,52.8,'X',false}};
使用结构体数组可以用数组表示法,也可以用指针表示法
示例:(代码有问题)
#define_CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//超女基本信息结构体st_girl,存放了超女全部的数据项
struct st_girl
{
string name[21]; //姓名
int age; //年龄
double weight ; //体重(kg)
char sex ; //性别:X-女;Y-男
bool yz; //颜值:true-漂亮;false-不漂亮
};
//void func(const st_girl* pst)
//{
// cout << "姓名:" << pst->name << ",年龄:" << pst->age
// << ",体重:" << pst->weight << ",性别:" << pst->sex
// << ",颜值:" << pst->yz << endl;
//
//}
int main()
{
//struct st_girl stgirl; //创建结构体变量(在C语言中,必须写关键字struct,在C++中可以不写)
st_girl girls[3];
memset(girls, 0, sizeof(girls));
strcpy((girls + 0)->name, "西施"); (girls + 0)->age = 25; girls[0].weight = 45; girls[0].sex = 'X'; girls[0].yz = true;
girls[1] = { "西瓜",2,10.6,'Y',false };
*(girls + 2) = { "冰冰",23,50.3,'X',true };
for (int i = 0; i < 3; i++)
{
cout << "姓名:" << (girls + i)->name << ",年龄:" << (girls + i)->age
<< ",体重:" << (girls + i)->weight << ",性别:" << (girls + i)->sex
<< ",颜值:" << (girls + i)->yz << endl;
}
}
10.4结构体嵌入数组和结构体
示例:
#include<iostream>
using namespace std;
struct st_pet //宠物结构体
{
char name[21]; //宠物的姓名
char type[21]; //宠物的物种
};
//超女基本信息结构体st_girl,存放了超女全部的数据项
struct st_girl
{
char name[21]; //姓名
int age; //年龄
double weight ; //体重(kg)
char sex ; //性别:X-女;Y-男
bool yz; //颜值:true-漂亮;false-不漂亮
struct st_pet pet; //宠物
};
int main()
{
st_girl girl = { "西施",23,50.5,'X',true,{"宝宝","鸭子"} };
cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;
girl = { "西瓜",25,50.5,'X',true,{"贝贝","鸭子"} }; //C++11标准的语法
cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;
girl.pet = { "小白","狗" }; //C++11标准的语法
cout << "姓名:" << girl.name << "的宠物是一只" << girl.pet.type << ",名字叫" << girl.pet.name << "。" << endl;
}
*10.5结构体中的指针
如果结构体中的指针指向的是动态分配的内存地址:
- 对结构体用sizeof运算可能没有意义
- 对结构体用memset()函数可能会造成内存泄漏
- C++的字符串string中有一个指向的是动态分配的内存地址指针
struct string
{
char *ptr; //指向动态分配内存的地址
......
}
示例:
struct st_girl
{
string name; //超女姓名
};
int main()
{
st_girl girl; //创建结构体变量
girl.name = "西施";
cout << "girl.name=" << girl.name << endl;
girl.name = "好大的西瓜";
cout << "girl.name=" << girl.name << endl;
}
11.简单链表
链表是操作系统最重要的数据结构
链表有两种:单链表、双链表
示例:(有问题)
#include<iostream>
using namespace std;
//超女单链表
struct st_girl
{
int no; //超女编号
string name; //超女姓名
struct st_girl* next; //下一个超女节点的地址,如果本节点是最后一条记录,填nullptr
};
int main()
{
st_girl *head=nullptr,*tail=nullptr,*tmp=nullptr; //head头指针、tail尾指针、tmp临时指针
//分配第一个节点
tmp = new st_girl;
tmp->no = 1;
tmp->name="西施";
tmp->next=nullptr;
//分配第二个节点
//tmp = new st_girl({ 6,"冰冰",nullptr });
//tail->next = tmp; //把上一个节点的next指针指向新节点
//tail = tmp; //移动尾指针,让尾指针指向刚分配出来的新节点
tmp = new st_girl;
tmp->no = 6;
tmp->name = "冰冰";
tmp->next = nullptr;
//分配第三个节点
//tmp = new st_girl({ 3,"咪咪",nullptr });
//tail->next = tmp; //把上一个节点的next指针指向新节点
//tail = tmp; //移动尾指针,让尾指针指向刚分配出来的新节点
tmp = new st_girl;
tmp->no = 1;
tmp->name = "咪咪";
tmp->next = nullptr;
//遍历链表
tmp = head; //从头节点开始
while (tmp != nullptr)
{
cout << "no=" << tmp->no << "name=" << tmp->name << "tmp->next=" << tmp->next << endl;
tmp = tmp->next; //顺着next指向往后面找
}
//释放链表
while (head != nullptr)
{
tmp = head; //让临时节点指向头节点
head = head->next; //头节点后移
delete tmp; //删除临时节点
}
}
12.共同体
共同体(共用体、联合体)是一种数据格式,它能存储不同的数据类型,但是,在同一时间只能存储其中的一种类型
声明语法:
union 共同体
{
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
......
成员n的数据类型 成员名n;
}
注意:
- 共同体占用内存的大小是它最大的成员占用内存的大小(内存对齐)
- 全部的成员使用同一块内存
- 共同体中的值为最后被赋值的那个成员的值
- 匿名共同体没有名字,可以在定义的时候创建匿名共同体变量,也可以嵌入结构体中
应用场景:
- 当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间(嵌入式系统)
- 用于回调函数的参数(相当于支持多种数据类型)
示例:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//声明超女结构体
struct st_girl
{
int no; //超女编号
union
{
int a;
double b;
char c[21];
};
};
int main()
{
struct st_girl girl;
cout << "girl.a的地址是:" << (void*)&girl.a << endl;
cout << "girl.b的地址是:" << (void*)&girl.b << endl;
cout << "girl.c的地址是:" << (void*)&girl.c << endl;
girl.a = 3;
girl.b = 8.8;
strcpy(girl.c, "我是一直杀杀鸟");
cout << "girl.a=" << girl.a << endl;
cout << "girl.b=" << girl.b << endl;
cout << "girl.c=" << girl.c << endl;
}
13.枚举
创建常量的方法:
- 宏常量,用预处理指令#define创建
- 用const修饰的变量
- 枚举
示例:
int main()
{
enum colors{red=0,yellow=1,blue=2,other=3}; //创建枚举类型colors
colors cc = red; //创建枚举变量,并赋初始值(不能直接把0,1,2,3赋值给cc,只能用常量名)
cout << "red=" << red << ",yellow=" << yellow << ",blue=" << blue << ",other=" << other <<endl;
switch (cc)
{
case red: cout << "红色。\n"; break;
case yellow: cout << "黄色。\n"; break;
case blue: cout << "蓝色。\n"; break;
default: cout << "未知。\n";
}
}
语法:
enum 枚举名 {枚举量1,枚举量2,枚举量3,......,枚举量n};
例如:
enum colors{red,yellow,blue};
这条语句完成了两项工作:
- 让colors成了一种新的枚举类型的名称,可以用它创建枚举变量
- 将red、yellow、blue作为符号常量,默认值是整数的0、1、2
注意:
- 用枚举创建的变量取值只能在美剧范围之内
- 枚举的作用域与变量的作用域相同
- 可以显示的设置枚举量的值(必须是整数)
enum colors {red=1,yellow=2,blue=3};
- 也可以只显示的 指定某些枚举量的值(枚举量的值可以重复)
enum colors{red,yellow=0,blue};
- 可以将整数强制转换成枚举量 语法:枚举类型(整数)
14.引用
14.1引用的基本概念
引用变量是C++新增的复合类型
引用是已定义的变量的别名
引用的主要用途但是用作函数的形参和返回值
声明/创建引用的语法:数据类型&引用名=原变量名;
注意:
- 引用的数据类型要与原变量名的数据类型相同
- 引用名和原变量名可以互换,它们值和内存单元是相同的
- 必须在声明引用的时候初始化,初始化后不可以改变
- C和C++使用“&”符号来指示/取变量的地址,C++给“&”符号赋予了另一种含义
示例:
int main()
{
int a = 3; //声明普通的整型变量
int& ra = a; //创建引用ra,ra是a的别名
cout << "a的地址是:" << &a << ",a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
int b = 5;
ra = b; //把b的值赋给ra
cout << "b的地址是:" << &b << ",b的值是:" << b << endl;
cout << "a的地址是:" << &a << ",a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
}
14.2引用的本质
引用是指针常量的伪装
引用时编译器提供的一个有用且安全的工具,去除了指针的一些缺点,禁止了部分不安全的操作
变量是什么?变量就是一个在程序执行过程中可以改变的量
换一个角度,变量是一块内存区域的名字,它代表了这块内存区域,当我们对变量进行修改的时候,会引起内存区域中内容的改变
在计算机看来,内存区域根本就不存在什么名字,它仅有的标志就是它的地址,因此我们若想修改一块内存区域的内容,只有知道它的地址才能实现
所谓的变量只不过是编译器给我们进行的一种抽象,让我们不必去了解更多的细节,降低我们的思维跨度而已
示例:
int main()
{
int a = 3; //声明普通的整型变量
int& ra = a; //创建引用ra,ra是a的别名 把int&替换成int* const,把a替换成&a
int* const rb = &a; //声明指针常量rb,让它指向变量a
cout << "a的地址是:" << &a << ",a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl; //把&ra替换成ra,把ra替换成*ra
cout << "rb的地址是:" << rb << ",*rb的值是:" << *rb << endl;
ra = 5; //把5的值赋给ra
cout << "a的地址是:" << &a << ",a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
cout << "rb的地址是:" << rb << ",*rb的值是:" << *rb << endl;
}
14.3引用用于函数的参数
把函数的形参声明为引用,调用函数的时候,形参将成为实参的别名
这种方法也叫按引用转递或传引用(传值、传地址、传引用只是说法不同,其实都是传值)
引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参
- 传引用的代码更简洁
- 传引用不必使用二级指针
- 引用的属性和特别之处
示例1:
void func1(int** p) //传地址,实参是指针的地址,形参是二级指针
{
*p = new int(3); //p是二级指针,存放指针的地址
cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
}
void func2(int*& p) //传引用,实参是指针,形参是指针的别名
{
p = new int(3); //p是指针的别名
cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}
int main()
{
int* p = nullptr; //存放在子函数中动态分配内存的地址
func1(&p); //传地址,实参填指针p的地址
//func2(p); //传引用,实参填指针p
cout << "main内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}
14.4引用的形参和const
const的用途:
- 普通变量、指针、函数形参,加上const修饰表示不可改变
- 引用的形参的const有特别之处
如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量。让引用指向临时变量
什么时候将创建临时变量呢?
- 引用是const
- 数据对象的类型正确,但不是左值
- 数据对象的类型不正确,但可以传唤为正确的类型
结论:如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量。将实参的值传递给匿名变量,并让形参来引用该变量
将引用形参声明为const的理由有三个:
- 使用const可以避免无意中修改数据的编程错误
- 使用const使函数能够处理const和非const实参,否则将只能接受非const实参
- 使用const函数能正确生成并使用临时变量
左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针
非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式
示例:
void func1(const int no,const string str ) //传值
{
cout << "亲爱的" << no<< "号:" << str<< endl;
}
void func2(const int* no,const string* str) //传地址
{
cout << "亲爱的" << no << "号:" << *str << endl;
}
void func3(const int& no,const string& str) //传引用
{
cout << "亲爱的" << no << "号:" << str << endl;
}
int main()
{
int bh = 3; //超女的编号
string message = "我是一只z小鸟。";
func1(8, "我是一只小小鸟。");
//func1(bh, message); //传值
//func2(&bh, &message); //传地址
//func3(bh, message); //传引用
cout << "亲爱的" << bh << "号:" << message << endl;
}
14.5引用用于函数的返回值
传递的函数返回机制与值传递类似
函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值
double m=sqrt(36); //sqrt()是求平方根函数
sqrt(36)的返回值6被拷贝到临时的位置,然后赋值给m
cout<<sqrt(25);
sqrt(25)的返回值5被拷贝到临时的位置,然后传递给cout
如果返回的是一个结构体,将把整个结构体拷贝到临时的位置
如果返回引用不会拷贝内存
语法:
返回值的数据类型 & 函数名(形参列表);
示例:
int func1() //返回的是值
{
int i = 3;
return i;
}
int &func2() //返回的是引用
{
int i = 3;
return i;
}
int main()
{
int a = func1(); cout << "a=" << a << endl; //返回的是值
int& b = func2(); cout << "b=" << b << endl; //返回的是引用
}
注意:
- 如果返回局部变量的引用,其本质是野指针
- 可以返回函数的引用参数、类的成员、全局变量、静态变量
- 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型
示例:
const int &func2(int &ra) //返回的是引用
{
ra++;
cout << "ra的地址是:" << &ra << ",ra=" << ra << endl;
return ra;
}
int main()
{
int a = 3;
const int& b = func2(a);
cout << "a的地址是:" << &a << ",a=" << a << endl;
cout << "b的地址是:" << &b << ",b=" << b << endl;
}
14.6各种形参的使用场景
传值、传地址和传引用的指导原则《C++ Primer Plus》
void func(int a); //传值
void func(int* a); //传地址
void func(int& a); //传引用
1)如果不需要在函数中修改实参
- 如果实参很小,如内置数据类型或小型结构体,则按置传递
示例:
void func(int a)
{
memspy();
strncpy();
memset();
}
int main()
{
func(5);
}
- 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)
- 如果实参是较大大结构,则使用const指针或const引用
- 数据实参是类,则使用const引用,传递类的标准方式是按引用传递
2)如果需要在函数中修改实参
- 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x
- 如果实参是数组,则只能使用指针
- 如果实参是结构体,则使用指针或引用
- 如果实参是类,则使用引用
当然,这只是一些指导原则,很很可能有充分的理由做出其它的选择
例如:对于基本类型,cin使用引用,因此可以使用cin>>a,而不是cin>>&a
15.函数
15.1函数的默认参数
默认参数是指调用函数的时候,如果不写实参,那么将使用的一个缺省值
语法:返回值 函数名(数据类型 参数=值,数据类型 参数=值)
示例:
void func(const string& message="我喜欢你") //向超女表白的函数
{
cout << "亲爱的:" << message << endl;
}
int main()
{
func("我是一只傻傻鸟");
func();
}
注意:
- 如果函数的声明和定义是分开的,在函数声明中书写默认参数,函数的定义中不能书写默认参数
- 函数必须从右向左设置默认参数。也就是说,如果要为某个参数设置默认值,则必须为它右边所有的参数提供默认值
- 调用函数的时候,如果指定了某个参数的值,那么该参数前面所有的参数都必须指定
15.2函数重载
在C++中,不同的函数可以用相同的名字,这就叫函数的重载
函数重载(函数多态)是指设计一系列同名函数,让他们完成相同d(似)的工作
C++允许定义名称相同的函数,条件是它们的特征(形参的个数、数据类型和排列顺序)不同
#1 int func(short a ,string b);
#2 int func(int a ,string b);
#3 int func(double a ,string b);
#4 int func(int a ,string b,int len);
#5 int func(short b ,int a);
调用重载函数的时候,在代码中我们用相同的函数名,但是,后面的实参不一样,编译器根据实参与重载函数的形参进行匹配,然后决定调用具体的函数,如果匹配失败,编译器将视为错误
在实际开发中,视需求重载各种数据类型,不要重载功能不同的函数
示例:
void myswap(int& a,int& b) //交换两个字符串变量的值
{
int tmp = a; a = b; b = tmp;
}
void myswap(string& a, string& b) //交换两个字符串变量的值
{
string tmp = a; a = b; b = tmp;
}
int main()
{
int a = 3, b = 5;
myswap(a, b);
cout << "a=" << a << ",b=" << b << endl;
string c = "西施", d = "西瓜";
myswap(c, d);
cout << "c=" << c << ",d=" << d << endl;
}
注意:
- 使用重载函数时,如果数据类型不匹配,C++尝试使用类型转换与形参进行匹配,如果转换后有多个函数能匹配上,编译将报错
- 引用可以作为函数重载的条件,但是,调用重载函数的时候,如果实参是变量,编译器形参类型的本身和类型引用视为同一特征
- 如果重载函数有默认参数,调用函数时,可能导致匹配失败
- const不能作为函数重载的特征
- 返回值不能作为函数重载的特征
- C++的名称修饰:编译时,对每个函数名进行加密,替换成不同名的函数
void MyFunctionFoo(int,float);
void MyFunctionFoo(long,float);
? MyFunctionFoo@@YAXH(int,float);
void MyFunctionFoo^$@(long,float);
15.3内联函数
C++将内联函数的代码组合到程序中,可以提高程序运行的速度
语法:在函数声明和定义前加上关键字inline
通常的做法是将函数声明和定义写在一起
注意:
- 内联函数节省时间,但消耗内存
- 如果函数过大,编译器可能不将其作为内联函数
- 内联函数不能递归
示例:
inline void show(const short bh, const string message); //表白函数
int main()
{
//show(3, "我错了");
{
int bh = 3;
string message = "我错了";
cout << "亲爱的" << bh << "号:" << message << endl;
}
//show(8, "我没错");
{
int bh = 8;
string message = "我没错";
cout << "亲爱的" << bh << "号:" << message << endl;
}
//show(5, "我不认错");
{
int bh = 5;
string message = "我不认错";
cout << "亲爱的" << bh << "号:" << message << endl;
}
}
inline void show(const short bh, const string message)
{
cout << "亲爱的" << bh << "号:" << message << endl;
}
15.4构造函数和析构函数
构造函数:在创建对象时,自动的进行初始化工作
析构函数:在销毁对象前,自动的完成清理工作
1)构造函数
语法:类名(){......}
- 访问权限必须是public
- 函数名必须与类名相同
- 没有返回值,也不写void
- 可以有参数,可以重载,可以有默认参数
- 创建对象时会自动调用一次,不能手工调用
2)析构函数
语法:~类名(){......}
- 访问权限必须时public
- 函数名必须在类名前加~
- 没有返回值,也不写void
- 没有参数,不能重载
- 销毁对象前只会自动调用一次,但是可以手动调用
注意:
- 如果没有提供构造/析构函数,编译器将提供空实现的构造/析构函数
- 如果提供了构造/析构函数,编译器将不提供空的构造/析构函数
- 创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数
- 创建对象的时候不要在对象名后面加空的圆括号,编译器误认为时声明函数(没有构造函数、构造函数没有参数、构造函数的参数都有默认参数)
- 在构造函数名后面加括号和参数不是调用构造函数,时创建匿名对象
- 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)
CGirl girl=10;
- 一下两行代码有本质的区别:
CGirl girl=CGirl("西施"20); //显示创建对象
CGirl girl; //创建对象
girl=CGirl("西施"20); //创建匿名对象,然后给现有的对象赋值
- 用new/delete创建/销毁对象时,也会调用构造/析构函数
- 不建议在构造/析构函数中写太多的代码,可以调用成员函数
- 除了初始化,不建议让构造做太多工作(只能成功不会失败)
- C++11支持使用统一初始化列表
CGirl girl={"西施"20};
CGirl girl {"西施"20};
CGirl *girl=new CGirl{"西施"20};
- 如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构成员类
示例1:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
char m_memo[301]; //备注
CGirl() //没有参数的构造函数
{
m_name.clear(); m_age = 0; memset(m_memo, 0, sizeof(m_memo));
cout << "调用了CGirl()构造函数。\n";
}
CGirl(string name) //一个参数的构造函数
{
m_name.clear(); m_age = 0; memset(m_memo, 0, sizeof(m_memo));
cout << "调用了CGirl(name)构造函数。\n";
m_name = name;
}
CGirl(string name, int age) //两个参数的构造函数
{
m_name.clear(); m_age = 0; memset(m_memo, 0, sizeof(m_memo));
cout << "调用了CGirl(name age)构造函数。\n";
m_name = name;
}
~CGirl() //析构函数
{
cout << "调用了~CGirl()\n";
}
void show() //超女自我介绍的方法
{
cout << "姓名:" << m_name << ",年龄:" << m_age << ",备注:" << m_memo << endl;
}
};
int main()
{
CGirl girl; //创建超女对象,不设置任何初始值
//CGirl girl("西施",8); //创建超女对象,为成员姓名设置初始值
girl.show(); //显示超女的自我介绍
}
15.5拷贝构造函数
用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数
如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数,它的功能是把已存在对象的成员变量赋值给新对象的成员变量
示例:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
//没有参数的构造函数
CGirl() {m_name.clear(); m_age = 0; cout << "调用了CGirl()构造函数。\n"; }
//析构函数
~CGirl() {cout << "调用了~CGirl()\n";}
//超女自我介绍的方法,显示姓名和年龄
void show() {cout << "姓名:" << m_name << ",年龄:" << m_age << endl;}
};
int main()
{
CGirl gl;
CGirl g2=CGirl();
CGirl* g3=new CGirl;
CGirl* g4=new CGirl();
delete g3;
delete g4;
}
用一个已存在的对象创建新的对象语法:
类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;
拷贝构造函数的语法:
类名(const 类名&对象名){......}
示例:(有问题)
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
//没有参数的普通构造函数
CGirl() {m_name.clear(); m_age = 0; cout << "调用了CGirl()构造函数。\n"; }
//没有重载的拷贝构造函数(默认拷贝构造函数)
CGirl(const CGirl& gg) { m_name = "漂亮的"+gg.m_name; m_age = gg.m_age - 1; cout << "调用了CGirl(const CGirl)拷贝构造函数。\n"; }
//析构函数
~CGirl() {cout << "调用了~CGirl()\n";}
//超女自我介绍的方法,显示姓名和年龄
void show() {cout << "姓名:" << m_name << ",年龄:" << m_age << endl;}
};
int main()
{
CGirl gl;
g1.m_name = "西施"; g1.m_age = 23;
CGirl g2=g1;
g2.show();
}
注意:
- 访问权限必须是public
- 函数名必须与类名相同
- 没有返回值,不写void
- 如果类中定义了拷贝构造函数,编译器将不提供拷贝构造函数
- 以值传递的方式调用函数时,如果实参为对象,会调用拷贝构造函数
- 函数以值的方式返回对象时,可能会调用拷贝构造函数(VS会调用,Linux不会,g++编译器做了优化)
示例:(有点小问题)
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
//没有参数的普通构造函数
CGirl() {m_name.clear(); m_age = 0; cout << "调用了CGirl()构造函数。\n"; }
//没有重载的拷贝构造函数(默认拷贝构造函数)
CGirl(const CGirl& gg) { m_name = "漂亮的"+gg.m_name; m_age = gg.m_age - 1; cout << "调用了CGirl(const CGirl)拷贝构造函数。\n"; }
//析构函数
~CGirl() {cout << "调用了~CGirl()\n";}
//超女自我介绍的方法,显示姓名和年龄
void show() {cout << "姓名:" << m_name << ",年龄:" << m_age << endl;}
};
CGirl func()
{
CGirl gg;
gg.m_name = "西施"; gg.m_age = 23;
cout << "对象g的地址:" << &gg << endl;
return gg;
}
int main()
{
CGirl g = func();
g.show();
}
- 拷贝构造函数可以重载,可以有默认参数
类名(......,const 类名&对象名,......){......}
- 如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数
16.对象和类
面向对象编程
- 抽象
- 多态
- 代码重用
- 封装和数据隐藏
- 继承
16.1对象和类-从结构体到类
示例1:
struct st_girl //超女基本信息结构体st_girl,存放了超女全部的数据项
{
string name; //姓名
int age; //年龄
int height; //身高cm
double weight; //体重kg
char sex = 'X'; //性别:X-女,Y-男
int yz; //颜值:1-漂亮,2-一般,3-丑八怪
string special; //特长
string memo; //备注
};
void setvalue(st_girl& girl, string name, int age, int height, double weight,
char sex, int yz, string special, string memo)
{
girl.name = name;
girl.age = age;
girl.height = height;
girl.weight = weight;
girl.sex = sex;
girl.yz = yz;
girl.special = special;
girl.memo = memo;
}
void show(const st_girl& girl)
{
cout << "姓名:" << girl.name << ",年龄:" << girl.age << ",身高:" << girl.height
<< ",体重:" << girl.weight << ",性别" << girl.sex << ",颜值:" << girl.yz
<< ",特长" << girl.special << ",备注:" << girl.memo << endl;
}
int main()
{
st_girl girl;
setvalue(girl, "西施", 26, 170, 50.5, 'X', 1, "唱歌、跳舞、洗衣服", "春秋第一美女,四大美女之一");
show(girl);
}
示例2:
struct st_girl //超女基本信息结构体st_girl,存放了超女全部的数据项
{
string name; //姓名
int age; //年龄
void setvalue(string name1, int age1)
{
name = name1; age = age1;
}
void show()
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
int main()
{
st_girl girl; //创建结构体变量
girl.setvalue( "西施", 26); //设置成员变量的值
girl.show(); //显示超女的自我介绍
}
示例3:(类)
class CGirl //超女基本信息结构体st_girl,存放了超女全部的数据项
{
pubic:
string name; //姓名
int age; //年龄
void setvalue(string name1, int age1)
{
name = name1; age = age1;
}
void show()
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
int main()
{
CGirl girl; //创建结构体变量
girl.setvalue( "西施", 26); //设置成员变量的值
girl.show(); //显示超女的自我介绍
}
对面向对象编程来说,一切都是对象,对象用类来描述
类把对象的数据和操作数据的方法作为一个整体考虑
定义类的语法:
class 类名
{
public:
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
......
成员n的数据类型 成员名n;
}
注意:
- 类的成员可以是变量,也可以是函数
- 类的成员变量也叫属性
- 类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面
- 用类定义一个类的变量叫做创建(或实例化)一个对象
- 类的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同
16.2类的访问权限
类的成员有三种访问权限:public(公有的)、private(私有的)、protected(受保护的)
在类的内部(类的成员函数中),无论成员被声明为pulic还是privat,都是可以访问
在类的外部(定义类的代码之外),只能访问public成员,不能访问private、protected成员
在一个类体的定义中,private和public可以出现多次
结构体的成员缺省为pulic,类的成员缺省为private
private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public
16.3对象和类-简单使用类
编程思想和方法的改变,披着C++外衣的C程序员
- 类的成员函数可以直接访问该类其它的成员函数
- 类的成员函数可以重载
- 类指针的用法与结构体指针用法相同
- 类的成员可以是任意数据类型(类中枚举)
- 可以为类的成员指定缺省值(C++11标准)
- 类可以创建对象数组,就像结构体数组一样
- 对象可以作为参数传递给函数,一般用传地址和传引用
- 可以用new动态创建对象,用delete释放对象
- 一般不直接访问(读和写)对象的成员,可以用成员函数
- 对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数
- 对类和对象用sizeof运算符意义不大,一般不用
- 用结构体描述纯粹的数据,用类描述对象
- 在类的声明中定义的函数都将自动成为内联函数,在类的声明之外定义的函数如果使用了inline限定符,也是内联函数
- 为了区分类的成员变量和成员函数的形参,把成员变量名加m_ 前缀或 _ 后缀,如m_name或name_
- 类的分文件编写
*16.4-浅拷贝和深拷贝(面试出现概率高)
注意浅拷贝的两个问题:
- 其中一个对象修改了内存中的数据,会影响另一个对象
- 其中一个对象释放了内存,另一个对象的指针就成了野指针
示例:
class CGirl //超女类CGirl
{
public:
int* m_ptr; //指针成员,计划使用堆内存
//没有参数的普通构造函数
CGirl()
{
m_ptr = nullptr;
cout << "调用了CGirl()构造函数。\n";
}
//没有重载的拷贝构造函数(默认拷贝构造函数)
CGirl(const CGirl& gg) {
m_ptr =gg.m_ptr; //分配内存
cout << "调用了CGirl(const CGirl)拷贝构造函数。\n";
}
//析构函数
~CGirl()
{
delete m_ptr;
m_ptr = nullptr;
cout << "调用了~CGirl()\n";
}
void show()
{
cout <<",m_ptr=" << m_ptr << /*",*m_ptr"<< *m_ptr<<*/endl;
}
};
int main()
{
CGirl g1;
g1.m_ptr = new int(3);
g1.show();
CGirl g2(g1); *g2.m_ptr = 8;
g1.show();
g2.show();
}
示例:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
int* m_ptr; //指针成员,计划使用堆内存
//没有参数的普通构造函数
CGirl() { m_name.clear(); m_age = 0; m_ptr = nullptr; cout << "调用了CGirl()构造函数。\n"; }
//没有重载的拷贝构造函数(默认拷贝构造函数)
CGirl(const CGirl& gg) {
m_name = gg.m_name; m_age = gg.m_age;
m_ptr = new int; //分配内存
//*m_ptr = *gg.m_ptr; //拷贝数据
memcpy(m_ptr, gg.m_ptr, sizeof(int)); //拷贝数据
cout << "调用了CGirl(const CGirl)拷贝构造函数。\n";
}
//析构函数
~CGirl() { delete m_ptr; m_ptr = nullptr; cout << "调用了~CGirl()\n"; }
//超女自我介绍的方法,显示姓名和年龄
void show() {
cout << "姓名:" << m_name << ",年龄:" << m_age << ",m_ptr=" << m_ptr << ",*m_ptr"<< endl;}
};
int main()
{
CGirl g1;
g1.m_name = "西施"; g1.m_age = 23; g1.m_ptr = new int(3);
g1.show();
CGirl g2(g1); *g2.m_ptr = 8;
g1.show();
g2.show();
}
16.5初始化列表
构造函数的执行可以分成两个阶段:初始化阶段和计算阶段。初始化阶段先于计算阶段
- 初始化阶段:全部的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中
- 计算阶段:一般是指用于执行构造函数体内的赋值操作
初始化列表的语法:
类名(形参列表):成员一(值一),成员二(值二),......,成员n(值n)
{......}
示例:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_age; //年龄
//没有参数的普通构造函数
CGirl() :m_name("西施"), m_age(23)
{
cout << "调用了CGirl()构造函数。\n";
}
//两个参数的构造函数
CGirl(string name,int age)
{
m_name = name;
m_age = age;
cout << "调用了CGirl(const CGirl)拷贝构造函数。\n";
}
//超女自我介绍的方法,显示姓名和年龄
void show()
{
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
};
int main()
{
CGirl g1;
g1.show();
}
注意:
- 如果成员已经在初始化列表中,则不应该在构造函数中再次赋值
- 初始化列表的括号中可以是具体的值,也可以是构造函数的形参名,还可以是表达式
- 初始化列表与赋值有本质的区别,如果成员是类,使用初始化列表调用的是拷贝构造函数,而赋值则是先创建对象(调用普通构造函数),然后再赋值
- 如果成员是类,初始化列表对性能略有提升
- 如果成员是常量和引用,必须使用初始化列表,因为常量和引用只能在定义的时候初始化
- 如果成员是没有默认构造函数的类,则必须使用初始化列表
- 拷贝构造函数也可以有初始化列表,但极少使用
- 类的成员变量可以不出现在初始化列表中
16.6const修饰成员函数
在类的成员函数后面加const关键字,表示在成员函数中保证不会修改调用对象的成员变量
示例:
class CGirl //超女类CGirl
{
public:
mutable string m_name; //姓名
int m_age; //年龄
//两个参数的构造函数
CGirl(const string &name,int age)
{
m_name = name;m_age = age;
cout << "调用了CGirlname,age)构造函数。\n";
}
//超女自我介绍的方法,显示姓名和年龄
void show() const //看到const时说明不可修改变量
{
m_name = "西施"; //上面声明时加mutble时可用,即可修改变量
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
};
int main()
{
CGirl g1("冰冰",18);
//g1.show();
}
注意:
- mutable可以突破const的限制,被mutable修改的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改
- 非const成员函数可以调用const成员函数和非const成员函数
- const成员函数不能调用非const成员函数
- 非const对象可以调用const修饰的成员函数和非const修饰的成员函数
- const对象只能调用const修饰的成员函数,不能调用非const修饰的成员函数
这里出现了令人纠结的三个问题:
1、为什么要保护类的成员变量不被修改?
2、为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线?
3、到底有没有必要使用const和mutable这两个关键字?
保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确地预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编辑器和建模工具去做,从而减轻程序员的负担。
16.7静态成员
-
类的静态成员包括静态成员变量和静态成员函数
-
用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全
-
用static关键字把类的成员变量声明为静态,表示它在程序中(不仅是对象)是共享的
-
静态成员变量不会在创建对象的时候初始化,必须在程序的全局区用代码清晰的初始化(用范围解析运算符"::")
-
静态成员使用类名加范围解析运算符::就可以访问,不需要创建类对象
-
如果把类的成员声明为静态的,就可以把它与类的对象独立开来(静态成员不属于对象)
-
静态成员变量在程序中只有一份(生命周期与程序运行期相同,存放在静态存储区的),不论是否创建了类的对象,也不论创建了多少个类的对象
-
静态成员函数只能访问静态成员,不能访问非静态成员
-
静态成员函数中没有this指针
-
非静态成员函数可以访问静态成员
-
私有静态成员变量在类外无法访问
-
const静态成员变量可以在定义类的时候初始化
示例:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
static int m_age; //年龄
//两个参数的构造函数
CGirl(const string& name, int age){m_name = name; m_age = age;}
//显示姓名
void showname() {cout << "我是:" << m_name << ",最漂亮的女朋友" << endl;}
//显示年龄
static void shownage() { cout << "今年" << m_age << "岁" << endl; }
};
int CGirl::m_age=18; //初始化类的静态成员变量 注:这行代码必须放在程序的全局区
int main()
{
CGirl::shownage();
cout << "CGirl::m_age=" << CGirl::m_age << endl;
CGirl g1("婷婷", 16);
g1.showname();
g1.shownage();
}
16.8this指针
如果类的成员函数中涉及多个对象,在这种情况下需要使用this指针
this指针存放了对象的地址,被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)
每个成员函数(包括构造函数和析构函数)都有一个this指针,可以用它访问调用者对象的成员(可以解决成员变量名与函数形参名相同的问题)
*this可以表示整个对象
如果在成员函数的括号后面使用const,那么将不能通过this指针修改成员变量
示例:
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
int m_yz; //颜值:1-沉鱼落雁,2-漂亮,3-一般,4-歪瓜裂枣
//两个参数的构造函数
CGirl(const string& name, int yz)
{
m_name = name; m_yz = yz;
}
//超女自我介绍的方法,
void show() const //看到const时说明不可修改变量
{
cout << "我是:" << m_name << ",最漂亮的超女" << endl;
}
const CGirl& pk(const CGirl&g)const
{
if (g.m_yz < m_yz)return g;
return *this; //this--自己的意思。谁调用了成员函数,this指针就指向谁
}
};
int main()
{
//比较超女的颜值,然后由更漂亮的超女做自我介绍
CGirl g1("婷婷",1),g2("西瓜",3),g3("冰冰",4),g4("小小",5), g5("花花",6);
const CGirl& g = g1.pk(g2).pk(g2).pk(g3).pk(g4).pk(g5);
g.show();
}
16.9简单对象模型
在C语言中,数据和处理数据的操作(函数)是分开的。也就是说,C语言本身没有支持数据和函数之间的关联性
C++用类描述抽象数据类型(abstract data type ,ADT),在类中定义了数据和函数,把数据和函数关联起来
对象中维护了多个指针表,表中放了成员与地址的对应关系
class CGirl //超女类CGirl
{
public:
string m_name; //姓名
static int m_age; //年龄
//默认构造函数和析构函数
CGirl(){memset(m_name,0,sizeof(m_name)); m_age = 0;}
~CGirl(){}
//显示姓名
void showname() {cout << "姓名:" << m_name<< endl;}
//显示年龄
void shownage() { cout << "年龄" << m_age << endl; }
};
C++类中有两种数据成员:nonstatic、static。三种函数成员:nonstatic、static、virtual
- 对象的内存大小包括:1)所有非静态数据成员的大小;2)由内存对齐而填补的内存大小;3)为了支持virtual成员而产生的额外负担
- 静态成员变量属于类,不计算在对象大小之内
- 成员函数是分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算在对象大小之内
- 用空指针可以访问没有用到this指针的非静态成员函数
- 空对象的大小为1字节。在C++中空类会占一个字节,这是为了让对象的实例能够相互区别
16.10友元
如果要访问类的私有成员变量,调用类的公有成员函数是唯一的方法,而类的私有成员函数则无法访问
友元提供了另一访问类的私有成员的方案。友元有三种:
1)友元全局函数
在友元全局函数中,可以访问另一个类的所有成员
示例:
class CGirl //超女类CGirl
{
friend int main();
public:
string m_name; //姓名
//两个参数的构造函数
CGirl(){m_name = "婷婷"; m_xw = 36; }
//显示姓名的成员函数
void showname() {cout << "我是:" << m_name << ",最漂亮的女朋友" << endl;}
private:
int m_xw; //胸围
//显示雄伟的成员函数
void showxw() { cout << "胸围是" << m_xw <<"D" << endl; }
};
int main()
{
CGirl g;
g.showname();
g.showxw();
}
2)友元类
在友元类所有成员函数中,可以访问另一个类的所有成员
注意事项:
- 友元关系不能被继承
- 友元关系是单向的,不具备交换性
若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明
示例:
class CGirl //超女类CGirl
{
friend class CBoy;
public:
string m_name; //姓名
//两个参数的构造函数
CGirl(){m_name = "婷婷"; m_xw = 36; }
//显示姓名的成员函数
void showname() {cout << "我是:" << m_name << ",最漂亮的女朋友" << endl;}
private:
int m_xw; //胸围
//显示雄伟的成员函数
void showxw()const { cout << "胸围是" << m_xw <<"D" << endl; }
};
class CBoy //婷婷的男朋友类
{
public:
void main(const CGirl& g)
{
cout << "我女朋友的姓名是:" << g.m_name << endl;
cout << "她的胸围是:" << g.m_xw <<"D"<< endl;
}
};
int main()
{
CGirl g;
CBoy b;
b.main(g);
}
3)友元成员函数
在友元类其成员函数中,可以访问另一个类的所有成员
如果要把男朋友类CBoy的某成员函数声明为超女类CGirl的友元,生命和定义的顺序如下:
class CGirl; //前置声明
class CBoy{......};
class CGirl{......};
//友元成员函数的定义
void CBoy::func(CGirl &g){......}
示例:
class CGirl; //把女友类声明前置
class CBoy //婷婷的男朋友类
{
public:
void func1(const CGirl& g);
void func2(const CGirl & g);
};
class CGirl //女友类CGirl
{
friend void CBoy::func1(const CGirl& g);
friend void CBoy::func2(const CGirl& g);
public:
string m_name; //姓名
//默认构造函数
CGirl(){m_name = "婷婷"; m_xw = 36; }
//显示姓名的成员函数
void showname() {cout << "我是:" << m_name << ",最漂亮的女朋友" << endl;}
private:
int m_xw; //胸围
//显示胸围的成员函数
void showxw()const { cout << "胸围是" << m_xw <<"D" << endl; }
};
void CBoy::func1(const CGirl& g) { cout << "我女朋友的姓名是:" << g.m_name << endl; }
void CBoy::func2(const CGirl& g) { cout << "她的胸围是:" << g.m_xw << "D" << endl; }
int main()
{
CGirl g;
CBoy b;
b.func1(g);
b.func2(g);
}
17.使用类-运算符重载
- 在C语言中,运算符已经被重载(*表示乘法和解引用)
- C++将运算符重载扩展到自定义的数据类型
- 给运算符赋予新的含义,让对象操作更美观(简单易用)
17.1运算符重载基础
C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观
例如字符串string用括号(+)拼接、cout用两个左尖括号(<<)输出
示例:
class CGirl //女友类CGirl
{
friend void operator+(CGirl& g, int score); //方法一;把函数声明为女友类的友元
friend void operator+(CGirl& g, int score);
private:
int m_xw; //胸围
int m_score; //分数
public:
string m_name; //姓名
//默认构造函数
CGirl() { m_name = "婷婷"; m_xw = 36; m_score = 30; }
//自我介绍的方法
void show() {cout << "我是:" << m_name << ",胸围是:" <<m_xw<<"D" << ",评分:" << m_score << endl; }
};
//方法一
void operator+(CGirl& g, int score)//第一个形参是女友对象的引用;第二个形参是女友的得分
{
g.m_score = g.m_score + score;
}
void operator-(CGirl& g, int score)
{
g.m_score = g.m_score + score;
}
int main()
{
//导演的要求:每轮表演之后,给女朋友加上她的得分
//方法:1、写一个友元的全局函数;2、给女友类增加一个成员函数
CGirl g;
//方法一
//operator+(g, 30); //调用给女友加分的函数operator+.
g + 30; //写成operator+或者-的格式,可以把调用函数的代码写成+或-的形式
g.show(); //调用女友自我介绍的方法show(),把全部的成员变量的值显示出来
g - 10;
g.show();
}
运算符重载函数的语法:返回值 operator 运算符(参数列表)
运算符重载函数的返回值类型要与运算符本身的含义一致
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象
如果同时重载了非成员函数和成员函数版本,会出现二义性
注意:
- 返回自定义数据类型的引用可以让多个运算符表达式串联起来(不要返回局部变量的引用)
- 重载函数参数列表中顺序决定了操作数的位置
- 重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符
- 如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷
- 重载函数不能违背运算符原来的含义和优先级
- 不能创建新的运算符
- 以下运算符不可重载:sizeof(sizeof运算符)、.(成员运算符)、.*(成员指针运算符)、::(作用域解析运算符)、?:(条件运算符)、typeid(一个RTTI运算符)、const_cast(强制类型转换运算符)、dynamic_cast(强制类型转换运算符)、static_cast(强制类型转换运算符)
- 以下运算符只能通过成员函数进行重载:=(赋值运算符)、()(函数调用运算符)、[](下标运算符)、->(通过指针访问类成员的运算符)
17.2重载左移运算符
- 重载左移运算符(<<)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)
- 只能使用非成员函数版本
- 如果要输出对象的私有成员,可以配合友元一起使用
示例:
class CGirl //女友类CGirl
{
friend ostream& operator<<(ostream& cout, const CGirl& g);
string m_name; //姓名
int m_xw; //胸围
int m_score; //分数
public:
//默认构造函数
CGirl() { m_name = "婷婷"; m_xw = 36; m_score = 30; }
//自我介绍的方法
void show() {cout << "我是:" << m_name << ",胸围是:" <<m_xw<<"D" << ",评分:" << m_score << endl; }
};
ostream& operator<<(ostream& cout, const CGirl& g)
{
cout << "我是:" << g.m_name << ",胸围是:" << g.m_xw << "D" << ",评分:" <<g. m_score << endl;
return cout;
}
int main() //实现重载左移运算符功能的方法:全局函数
{
CGirl g;
cout << g << endl;
}
17.3重载下标运算符
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便
下标运算符必须以成员函数的形式进行重载
下标运算符重载函数的语法:
返回值类型&perator[] (参数);
或者:
const 返回值类型&operator[] (参数) const;
示例:
class CGirl //女友类CGirl
{
private:
string m_boys[3]; //婷婷的男朋友
public:
string m_name; //姓名
//默认构造函数
CGirl() { m_boys[0] = "小华子"; m_boys[1] = "西门庆"; m_boys[2] = "安安"; }
//显示全部男友的姓名
void show() {cout<<"婷婷的男朋友有:" << m_boys[0] << "、" << m_boys[1] << "、" << m_boys[2] << endl; }
string &operator[](int i)
{
return m_boys[i];
}
};
int main()
{
CGirl g;
g[0] = "小华";
cout << "婷婷最帅的男朋友是:" << g[0] << endl;
g .show();
}
- 使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素
- 使用第二种声明方式,[]只能访问而不能修改元素
- 在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const对象只能调用const成员函数,如果不提供第二种形式,那么无法访问const对象的任何数组元素
- 在重载函数中,可以对下标做合法性检查,防止数组越界
17.4重载赋值运算符
C++编译器可能会给类添加四个函数:
- 默认构造函数,空实现
- 默认析构函数,空实现
- 默认拷贝构造函数,对成员变量进行浅拷贝
- 默认赋值函数,对成员变量进行浅拷贝
对象的赋值运算是用一个已经存在的对象,给另一个已经存在的对象赋值
如果类的定义中没有重载赋值函数,编译器就会提供一个默认赋值函数
如果类中重载了赋值函数,编译器将不提供默认赋值函数
重载赋值函数的语法:类名 & operator=(const 类名 & 源对象)
注意:
- 编译器提供的默认赋值函数,是一种浅赋值行为
- 如果对象中不存在堆区内存空间,默认重载赋值函数可以满足需求,否则需要深赋值
- 赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,使用已存在的对象进行构造;赋值运算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量
示例:
class CGirl //女友类CGirl
{
public:
string m_name; //姓名
int m_bh; //编号
int* m_ptr; //计划使用堆区内存
CGirl() { m_ptr = nullptr; }
~CGirl() { if (m_ptr)delete m_ptr; }
//显示全部成员变量
void show() {cout<<"编号:" << m_bh<< ",姓名:" << m_name <<",m_ptr="<<m_ptr<< endl; }
CGirl& operator=(const CGirl& g)
{
if (this == &g)return *this; //如果是自己给自己赋值
if (g.m_ptr == nullptr) //如果源对象的指针为空,则清空目标对象的内存和指针
{
if (g.m_ptr != nullptr) { delete m_ptr; m_ptr = nullptr; }
}
else//如果源对象的指针不为空
{
//如果目标对象的指针为空,先分配内存
if (m_ptr == nullptr)m_ptr = new int;
//然后,把源对象内存中的数据复制到目标对象的内存中
memcpy(m_ptr, g.m_ptr, sizeof(int));
}
m_bh = g.m_bh; m_name = g.m_name;
cout << "调用了重载赋值函数。\n" << endl;
return*this;
}
};
int main()
{
CGirl g1,g2; //创建超女对象
g1.m_bh = 8; g1.m_name = "婷婷"; g1.m_ptr = new int(3);
g1.show();
g2.show();
g2 = g1;
g2.show(); //相当于g2.m_bh=g1.m_bh;g2.m_name=g1.m_name;
cout << "*g1.m_ptr=" << *g1.m_ptr << "*g2.m_ptr=" << *g2.m_ptr << endl;
}
17.5重载new&delete运算符
重载new和delete运算符的目的是为了自定义内存分配的细节(内存池:快速分配和归还,无碎片)
建议先学习C语言的内存管理函数malloc()和free()
在C++中,使用new时,编译器做了两件事;
- 调用标准库函数operator new()分配内存
- 调用构造函数初始化内存
使用delete时,也做了两件事情:
- 调用析构函数
- 调用标准库函数operator delete()释放内存
构造函数和析构函数由编译器的调用,我们无法控制。但是,可以重载内存分配函数operator new()和释放函数operator delete()
1)重载内存分配函数的语法:void* operator new(size_t size);
参数必须时size_t,返回值必须是void*
2)重载内存释放函数的语法:void operator delete(void* ptr);
参数必须是void*(指向由operator new()分配的内存),返回值必须是void
重载的new和delete可以是全局函数,也可以是类的成员函数
为一个类重载new和delete时,尽管不必显式地使用static,但实际上仍在创建static成员函数
编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()
new[]和delete[]也可以重载
示例1:
//演示了整型变量动态分配内存
void* operator new(size_t size) //参数必须是size_t,返回值必须是void*
{
cout << "调用了重载的new:" << size << "字节。\n";
void* ptr = malloc(size); //申请内存
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "调用了重载的delete。\n";
if (ptr == 0)return; //对空指针delete是安全的
free(ptr); //释放内存
}
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void*)p1 << ",*p1=" << *p1 << endl;
delete p1;
}
示例2:
void* operator new(size_t size) //参数必须是size_t,返回值必须是void*
{
cout << "调用了全局重载的new:" << size << "字节。\n";
void* ptr = malloc(size); //申请内存
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "调用了全局重载的delete。\n";
if (ptr == 0)return; //对空指针delete是安全的
free(ptr); //释放内存
}
class CGirl //超女类CGirl
{
public:
int m_bh; //编号
int m_xw; //胸围
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() {cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) //参数必须是size_t,返回值必须是void*
{
cout << "调用了类的重载的new:" << size << "字节。\n";
void* ptr = malloc(size); //申请内存
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) //参数必须是void*,返回值必须是void
{
cout << "调用了类的重载的delete。\n";
if (ptr == 0)return; //对空指针delete是安全的
free(ptr); //释放内存
}
};
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void*)p1 << ",*p1=" << *p1 << endl;
delete p1;
CGirl* p2 = new CGirl(3, 8);
cout << "p2的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
delete p2;
}
17.6内存池
在实际开发中,重载new和delete运算符的主要目的是实现内存池,内存池在高性能的服务器中很常用
- 预先分配一大块的内存池
- 提升分配和归还的速度
- 减少内存碎片
示例:
class CGirl //超女类CGirl
{
public:
int m_bh; //编号
int m_xw; //胸围
static char* m_pool; //内存池的起始地址
static bool initpool() //初始化内存池的函数
{
m_pool = (char*)malloc(18); //向系统申请18字节的内存
if (m_pool == 0)return false; //如果申请内存失败,返回false
memset(m_pool, 0, 18); //把内存池中的内容初始化为0
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
static void freeool() //释放内存池
{
if (m_pool == 0)return; //如果内存池为空,不需要释放,直接返回
free(m_pool); //把内存池归还给系统
cout << "内存池已释放。\n";
}
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() {cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) //参数必须是size_t(unsigned long long ),返回值必须是void*
{
if (m_pool[0] == 0) //判断第一个位置是否空闲
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; //把第一个位置标记为已分配
return m_pool + 1; //返回第一个用于存放对象的地址
}
if (m_pool[0] == 0) //判断第二个位置是否空闲
{
cout << "分配了第二块内存:" << (void*)(m_pool + 1) << endl;
m_pool[9] = 1; //把第二个位置标记为已分配
return m_pool + 9; //返回第二个用于存放对象的地址
}
//如果以上两个位置都不可用,那就直接向系统申请内存
void* ptr = malloc(size); //申请内存
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) //参数必须是void*,返回值必须是void
{
if (ptr == 0)return; //如果传进来的地址为空,直接返回
if (ptr == m_pool + 1) //如果传进来的地址是内存池的第一个位置
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; //把第一个位置标记为空闲
return;
}
if (ptr == m_pool + 9) //如果传进来的地址是内存池的第二个位置
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; //把第二个位置标记为空闲
return;
}
//如果传进来的地址不属于内存池,把它归还给系统
free(ptr); //释放内存
}
};
//内存池的指针m_pool是静态成员变量,要在main函数外面初始化
char* CGirl::m_pool = 0; //初始化内存池的指针
int main()
{
//初始化内存池
if (CGirl::initpool() == false) { cout << "初始化内存池失败。\n"; return-1; }
CGirl* p1 = new CGirl(3, 8); //将使用内存池的第一个位置
cout << "p1的地址是:" << p1 << "编号:" << p1->m_bh << ",胸围:" << p1->m_xw << endl;
CGirl* p2 = new CGirl(4, 7); //将使用内存池的第二个位置
cout << "p2的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
CGirl* p3 = new CGirl(6, 9); //将使用系统的内存
cout << "p3的地址是:" << p3 << "编号:" << p3->m_bh << ",胸围:" << p3->m_xw << endl;
delete p1; //将释放内存池的第一个位置
CGirl* p4 = new CGirl(5, 3);
cout << "p4的地址是:" << p4 << "编号:" << p4->m_bh << ",胸围:" << p4->m_xw << endl;
delete p2; //将释放内存池的第二个位置
delete p3; //将释放系统内存
delete p4; //将释放内存池的第一个位置
CGirl::freeool(); //释放内存池
}
17.7重载括号与运算符
括号运算符()也可以重载,对象名可以当成函数来使用(函数对象、仿函数)
括号运算符重载函数的语法:返回值类型operator() (参数列表)
示例:
void show(string str) ///向婷婷表白的函数
{
cout << "普通函数:" << str << endl;
}
class CGirl //定义一个类
{
public:
void operator()(string str)
{
cout << "重载函数:" << str << endl;
}
};
int main()
{
show("我是一只傻傻鸟");
CGirl girl;
girl("我不是一只傻傻鸟");
}
注意:
-
括号运算符必须以成员函数的形式进行重载
-
括号运算符重载函数具备普通函数全部的特征
-
如果函数对象与全局函数同名,按作用域规则选择调用的函数
-
C++规定,括号运算符函数只能用类的成员函数重载,不能用全局函数
函数对象的用途:
- 表面像函数,部分场景中可以代替函数,在STL中得到广泛的应用
- 函数对象本质是类,可以用成员变量存放更多的信息
- 函数对象有自己的数据类型
- 可以提供继承体系
17.8重载一元运算符
可重载的一元运算符:++(自增)、--(自减)、!(逻辑非)、&(取地址)、~(二进制反码)、(解引用)、+(一元加)、-(一元求反)*
一元运算符通常出现在它们所操作的对象的左边
但是,自增运算符++和自减运算符--有前置和后置之分
为了解决这个问题,C++规定,在重载++或--时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数
成员函数版:CGirl &operator++(); //++前置
示例:
class CGirl //定义一个类
{
public:
string m_name; //姓名
int m_ranking; //排名
//默认构造函数
CGirl() { m_name = "婷婷"; m_ranking = 5; }
void show(){cout << "姓名:" << m_name<<",排名:"<<m_ranking << endl;}
//++前置的重载函数
CGirl &operator++()
{
m_ranking++; return* this;
}
//后置的重载函数
CGirl operator++(int)
{
CGirl tmp = *this;
m_ranking++;
return tmp;
}
};
int main()
{
CGirl g1,g2;
int i = 5,j=5;
int x = ++(++(++i));
cout << "x=" << x << ",i=" << i << endl;
int y = j++;
cout <<"y=" << y << ",j=" << j << endl;
CGirl g3 = ++(++(++g1));
cout << "g3.m_ranking=" << g3.m_ranking << ",g1.m_raning=" << g1.m_ranking << endl;
CGirl g4 = g2++;
cout << "g4.m_ranking=" << g4.m_ranking << ",g2.m_ranking" << g2.m_ranking << endl;
//前置
++(++(++i));
++(++(++g1));
//后置
((g2++)++)++;
g1.show();
}
*17.9自动类型转换
对于内置类型,如果两种数据类型是兼容的,C++可以自动转换,如果从更大的数据转换为更小的数,可能会被截断或损失精度。
long count=8; //int转换为long
double time=11; //int转换为double
int side=3.33 //double转换为int的3
C++不自动转换不兼容的类型,下面语句是非法的:
int*ptr=8;
不能自动转换时,可以使用强制类型转换:
int*p=(int *)8;
如果某种类型与类相关,从某种类型转换为类类型是有意义的
string str="我是一只傻傻鸟";
在C++中,一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。这一过程称为隐式转换,它是自动进行的,不需要显示强制类型转换
CGirl g=CGirl(8); //显示转换
CGirl g=8; //隐式转换
cGirl g; //创建对象
g=8; //用CGirl(8)创建对象,再赋值给g
多个参数的构造函数,除第一个参数外,如果其它参数由却省值,也可以作为转换函数
注意:
1)一个类可以有多个转换函数
2)多个参数的构造函数,除了第一个参数外,如果其它参数有缺省值,也可以作为转换函数
3)CGirl(int)的隐式转换的场景:
- 将CGirl对象初始化为int值 //CGirl g1=8;
- 将int值赋给CGirl对象时 //CGirl g1;g1=8;
- 将int值传递给接受CGirl参数的函数时
- 返回值被声明为CGirl的函数试图返回int值时
- 在上述任意一种情况下,使用可转换为int类型的内置类型时
4)如果自动类型转换有二义性,编译将报错
将构造函数用作自动类型转换函数似乎时一项不错的特性,但有时候会导致意外的类型转换。explicit关键字用于关闭这种自动特性,但仍允许显示转换
如:
explicit CGirl(int bh);
CGirl g=8; //错误
Girl g=CGirl(8); //显示转换,可以
CGirl g=(CGirl)8; //显示转换,可以
在实际开发中,如果强调的是构造,建议使用eplicit,如果强调的是类型转换,则不使用explicit
示例:
class CGirl
{
public:
int m_bh; //编号
string m_name; //姓名
double m_weight; //体重kg
//默认构造函数
CGirl() { m_bh = 0; m_name.clear(); m_weight = 0; cout << "调用了CGirl()\n"; }
//自我介绍的方法
void show() { cout << "bh=" << m_bh << ",name=" << m_name << ",weight=" << m_weight << endl; }
CGirl(int bh){ m_bh = bh; m_name.clear(); m_weight = 0; cout << "调用了CGirl(int bh)\n";}
};
int main()
{
CGirl g1(8); //常规的写法
//CGirl g1 = CGirl(8); //显式转换
//CGirl g1 = 8; //隐式转换
//CGirl g1; //创建对象
//g1 = 8; //用CGirl(8)创建临时对象,再赋值给g
g1.show(); //
}
17.10转换函数
构造函数只用于从某种类型到类类型的转换,如果要进行相反的转换,可以使用特殊的运算符函数——转换函数
语法:operator 数据类型();
注意:转换函数必须时类的成员函数;不能指定返回值类型;不能有参数
可以让编译器决定选择转换函数(隐式转换),可以像使用强制类型转换那样使用它们(显示转换)
//三种写法
int i=girl; //隐式转换
int i=(int)girl; //显示转换
int i=int(gril); //显示转换
示例:
class CGirl
{
public:
int m_bh; //编号
string m_name; //姓名
double m_weight; //体重kg
//默认构造函数
CGirl() { m_bh = 8; m_name="婷婷"; m_weight = 50.7; }
operator int() { return m_bh; }
operator string() { return m_name; }
operator double() { return m_weight; }
};
int main()
{
CGirl g;
int a = int(g); cout << "a的值是:" << a << endl;
string b =string( g); cout << "b的值是:" << b << endl;
double c = double(g); cout << "c的值是:" << c << endl;
}
如果隐式转换存在二义性,编译器将报错
在C++98中,关键字explicit不能有于转换函数,但C++11消除了这种限制。因此,在C++11中可以将转换运算符声明为显式
还有一种方法:用一个功能相同的非转换函数替换转换函数,只要当函数被显式的调用时才会执行
int i=girl.to_int();
警告:应谨慎的使用隐式转换函数,通常,最好选择仅在被显式地调用时才会执行的函数`
18.类继承
- 重用经过测试的、可靠的代码,比从头开始写代码要好得多
- 类继承的目的是代码重用
示例:
class CAIIComers //海报报名者类
{
public:
string m_name; //姓名
string m_tel; //联系电话
//默认构造函数
CAIIComers() { m_name="某女"; m_tel = "不知道"; }
//报名时需要唱一首歌
void sing() { cout << "我是一只小小鸟。\n"; }
//设置姓名
void setname(const string& name) { m_name = name; }
//设置电话
void settel(const string&tel) { m_tel = tel; }
};
//被继承的类称为基类或父类,继承的类称为派生类或子类
//CAIIComers--基类;CGirl--派生类
class CGirl :public CAIIComers //:--表示继承;publie--继承方式;CAIIComers--被继承的类的类名
{
public:
int m_bh; //编号
CGirl() { m_bh = 8; }
void show() { cout << "编号:" << m_bh << ",姓名:" << m_name << ",联系电话:" << m_tel << endl; }
};
int main()
{
CGirl g;
g.setname("婷婷");
g.show();
}
18.1继承的基本概念
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程
语法:
class 派生类名: [继承方式] 基类名
{
派生类新增加的成员
};
被继承的类称为基类或父类,继承的类称为派生类或子类
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能
使用继承的场景:
1)如果新创建的类与现有的类相似,只是多处若干成员变量或成员函数时,可以使用继承
2)当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员提取出来,定义为基类,然后从基类继承
示例:
class Sort { //排序算法的基类
int m_data[30]; //待排序的数组
void print(); //显示排序后的结果
};
class BubbleSort :public Sort { //冒泡排序
void sort(); //对数组m_date中的元素进行排序
};
class ShellSort :public Sort { //希尔排序
void sort(); //对数组m_data中的元素进行排序
};
18.2继承方式
类成员的访问权限由高到低依次为:public-->protected-->private,public成员在类外可以访问,private成员只能在类的成员函数中访问
如果不考虑继承关系,protected成员和private成员一样,;类外不能访问。但是,当存在继承关系时,protected和private就不一样了。基类中的protected成员可以在派生类中访问,而基类中的private成员不能在派生类中访问
继承方式有:pubic(公有的)、protected(受保护的)、private(私有的)
它是可选的,如果不写,那么默认为private。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限
1)基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为protected时,那么基类成员在派生类中的访问权限最高也为protected,高于protected的会降级为protected,但低于protected不会升级。再如当继承方式为public时,那么基类成员在派生类中的访问权限将保持不变
也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的
2)不管继承方式如何,基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)
3)如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public或protected;只有那些不希望在派生类中使用的成员才声明为private
4)如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为protected
由于private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以,在实际开发中,一般使用public
在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员
使用using关键字可以改变基类成员在派生类中的访问权限
注意:using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中private成员在派生类中是不可见的,根本不能使用
18.3继承的对象模型
- 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数
- 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数
- 创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,this指针相同的
- 创建派生类对象时,先初始化基类对象,再初始化派生类对象
- 在VS中,用cl.exe可以查看类的内存模型
- 对派生类对象用sizeof得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小
- 在C++中,不同继承方式的访问权限只是语法上的处理
- 对派生类对象用memset()会清空基类私有成员
- 用指针可以访问到基类中的私有成员(没有内存对齐,没有占位符)
在Win中查看对象内存布局的方法
cl 源文件名 /d1 reportSingleClassLavout 类名
注意:类名不要太短,否则屏幕会显示一大堆东西,找起来很麻烦
例如,查看B类,源代码文件是demo01.cpp:
cl demo01.cpp /d1 reportSingleClassLayoutB
cl命令环境变量:
示例1:
class A { //基类
public:
A() { cout << "调用了A的构造函数。\n"; }
~A() { cout << "调用了A的析构函数。\n"; }
};
class B:public A //基类
{
public:
B() { cout << "调用了B的构造函数。\n"; }
~B() { cout << "调用了B的析构函数。\n"; }
};
class C :public B //孙类
{
public:
C() { cout << "调用了C的构造函数。\n"; }
~C() { cout << "调用了C的析构函数。\n"; }
};
int main()
{
A a; //B b、C c
}
示例2:
void* operator new(size_t size) //申请new运算符
{
void* ptr = malloc(size); //申请内存
cout << "申请到的内存的地址是:" << ptr << ",大小是:" << size << endl;
return ptr;
}
void operator delete(void*ptr) //重载delete运算符
{
if (ptr == 0)return; //对空指针delete是安全的
free(ptr); //释放内存
cout << "释放了内存。\n";
}
class A {
public:
int m_a = 10;
protected:
int m_b = 20;
private:
int m_c = 30;
public:
A() {
cout << "A由this指针是:" << this << endl;
cout << "A由m_a的地址是:" << &m_a<< endl;
cout << "A由m_b的地址是:" << &m_b << endl;
cout << "A由m_c的地址是:" << &m_c << endl;
}
void func() { cout << "m_a=" << m_a<<",m_b="<<m_b<<",m_c="<<m_c << endl; }
};
class B :public A //派生类
{
public:
int m_d = 40;
B() {
cout << "B由this指针是:" << this << endl;
cout << "B由m_a的地址是:" << &m_a << endl;
cout << "B由m_b的地址是:" << &m_b << endl;
//cout << "B由m_c的地址是:" << &m_c << endl;
cout << "B由m_d的地址是:" << &m_d << endl;
}
void func1() { cout << "m_d=" << m_d << endl; }
};
int main()
{
cout << "基类占用内存的大小是:" << sizeof(A) << endl;
cout << "派生类占用内存的大小是:" << sizeof(B) << endl;
B* p = new B;
p->func(); p->func1();
//memset(p,0,sizeof(B));
*((int*)p + 2) = 31; //把基类私有成员m_c的值修改成31----在实际开发中不建议使用
p->func(); p->func1();
delete p;
}
18.4如何构造基类
派生类构造函数要点如下:
-
创建派生类对象时,程序首先调用基类构造函数
-
如果没以指定基类构造函数,将使用基类的默认构造函数
-
可用初始化列表指明要使用的基类构造函数
-
基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员
-
派生类的构造函数总是调用一个基类构造函数,包括拷贝函数
-
在类的继承中,有一个原则:基类的成员变量必须由基类的构造函数初始化
-
派生类新增的成员变量由派生类的构造函数初始化
-
派生类不能一起初始化基类的成员变量。原因:
-
1)基类的私有成员在派生类中是不可见的,没办法初始化;
-
2)抛开基类的私有成员的因素,如果把初始化基类成员的代码写在派生类中,当基类被多个派生类继承的时候,每个派生类中都要初始化基类的成员,会导致代码重复
-
示例:
class A { //基类
public:
int m_a;
private:
int m_b;
public:
A() :m_a(0), m_b(0) //基类的默认构造函数
{
cout << "调用了基类的默认构造函数A().\n";
}
A(int a,int b) :m_a(a), m_b(b) //基类有两个参数的构造函数
{
cout << "调用了基类的默认构造函数A(int a,int b).\n";
}
A(const A&a) :m_a(a.m_a+1), m_b(a.m_b+1) //基类的拷贝构造函数
{
cout << "调用了基类的默认构造函数A(const A&a).\n";
}
//显示基类A全部的成员
void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
class B :public A //派生类
{
public:
int m_c;
B() :m_c(0), A() //派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)
{
cout << "调用了派生类的构造函数B().\n";
}
B(int a,int b,int c) : A(a,b),m_c(c) //指明用基类的有两个参数的构造函数
{
cout << "调用了派生类的构造函数B(int a,int b,int c).\n";
}
B(const A&a,int c) : A(a),m_c(c) //指明用基类的拷贝构造函数
{
cout << "调用了派生类的构造函数B(const A&a,int c).\n";
}
//显示派生类B全部的成员
void showB() { cout << "m_c=" << m_c << endl<< endl; }
};
int main()
{
B b1; //将调用基类默认的构造函数
b1.showA(); b1.showB();
B b2(1,2,3); //将调用基类有两个参数的构造函数
b2.showA(); b2.showB();
A a(10, 20); //创建基类对象
B b3(a,30); //将调用基类的拷贝构造函数
b3.showA(); b3.showB();
}
18.6名字遮蔽与类作用域
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的
基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数
类是一种作用域,每个类都有它自己的作用域,在这个作用域之内定义成员
在类的作用域之外,普通的成员只能通过对象(可以是对象本身,也可以是对象指针或对象引用)来访问,静态成员可以通过对象访问,也可以通过类访问
在成员前面加类名和域解析符可以省略不写
示例:
class A { //基类
public:
int m_a=30;
int m_b=50;
void func() { cout << "调用了A的func()函数。\n"; }
void func(int m_a) { cout << "调用了A的func(int a)函数。\n";A::m_a=m_a;this->m_a=m_a } //A::不能省略,因为与func()中的m_a重名,下面的可以
};
int main()
{
A a;
cout << "m_a的值是:" << a.A::m_a << endl;
a.func(); //a.A::func();
a.func(1); //a.A::func(1);
}
当存在继承关系时,基类的作用域嵌套派生类的作用域中。如果成员在派生类的作用域已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员
示例:
class A { //基类
public:
int m_a=10;
void func() { cout << "调用了A的func()函数。\n"; }
};
class B:public A { //子类
public:
int m_a = 20;
void func() { cout << "调用了B的func()函数。\n"; }
};
class C:public B { //孙类
public:
int m_a = 30;
void func() { cout << "调用了C的func()函数。\n"; }
};
int main()
{
C c;
cout << "C::m_a的值是:" << c.C::m_a << endl;
cout << "B::m_a的值是:" << c.B::m_a << endl;
cout << "A::m_a的值是:" << c.B::A::m_a << endl;
c.C::func();
c.B::func();
c.B::A::func();
}
18.7继承的特殊关系
派生类和基类之间有一些特殊关系:
- 如果继承方式是公有的,派生类对象可以使用基类成员
- 可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员
- 基类指针可以在不进行显式转换的情况下指向派生类对象
- 基类引用可以在不进行显式转换的情况下引用派生类对象
注意:
- 基类指针或引用只能调用基类的方法,不能调用派生类的方法
- 可以用派生类构造基类
- 如果函数的形参是基类,实参可以用派生类
- C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)
18.8多继承与虚继承
多继承的语法:
class 派生类名:[继承方式1] 基类名1,[继承方式2] 基类名2,......
{
派生类新增加的成员
};
菱形继承:
虚继承可以解决菱形继承的二义性和数据冗余的问题
有了多继承,就存在菱形继承,有了菱形继承就有虚继承,增加了复杂性
不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要用多继承
如果继承的层次很多、关系很复杂,程序的编写、调式和维护工作都会变得更加困难,由于这个原因,C++之后的很多面向对象的编程语言,例如java、C#、PHP等,都不支持多继承
19.多态
19.1多态的基本概念
基类指针只能调用基类的成员函数,不能调用派生类的成员函数
如果在基类的成员函数前加virtual关键字,把它声明为虚函数,基类指针就可以调用派生类的成员函数,通过派生类的成员函数,还可以访问派生对象的成员变量
有了虚函数,基类指针指向基类对象时就使用基类的成员函数,指向派生类对象时就使用派生类的成员函数,基类指针表现出了多种形式,这种现象称为多态
基类引用也可以使用多态
注意:
- 只需要在基类的函数声明中加上virtual关键字,函数定义时不能加
- 在派生类中重定义虚函数时,函数特征要相同
- 当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数
- 名字遮蔽和重载函数的规则也适用于虚函数
- 在派生类中重定义了虚函数的情况下,如果想使用基类的函数,可以加类名和域解析符
- 如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:1)效率更高;2)指出不要重新定义该函数
示例:
class CAIIComers { //报名者类
public:
int m_bh=0; //编号
virtual void show() { cout << "CAIIComers::show():我是"<<m_bh<<"号。"<<endl; }
};
class CGirl:public CAIIComers { //超女类
public:
int m_age = 0; //年龄
void show() { cout << "CGirl::show():我是" << m_bh << "号," << m_age << "岁。" << endl; }
};
int main()
{
CAIIComers a; a.m_bh = 3;
CGirl g;g.m_bh = 8; g.m_age = 23; //创建派生类对象并对成员赋值
CAIIComers* p; //声明基类指针
p = &a; p->show(); //让基类指针指向基类对象,并调用虚函数
p = &g; p->show(); //让基类指针指向派生类对象,并调用虚函数
}
19.2多态的应用场景
- 基类的虚函数实现基本功能
- 派生类重定义虚函数,扩展功能、提升性能
- 或者实现个性化的功能
示例:
class Hero //英雄基类
{
public:
int viability; //生存能力
int attack; //攻击伤害
virtual void skill1() { cout << "英雄释放了一技能。\n"; }
virtual void skill2() { cout << "英雄释放了二技能。\n"; }
virtual void ukill() { cout << "英雄释放了大绝招。\n"; }
};
class XS :public Hero //西施派生类
{
public:
void skill1() { cout << "西施释放了一技能。\n"; }
void skill2() { cout << "西施释放了二技能。\n"; }
void ukill() { cout << "西施释放了大绝招。\n"; }
};
class HX :public Hero //韩信派生类
{
public:
void skill1() { cout << "韩信释放了一技能。\n"; }
void skill2() { cout << "韩信释放了二技能。\n"; }
void ukill() { cout << "韩信释放了大绝招。\n"; }
};
class LB :public Hero //李白派生类
{
public:
void skill1() { cout << "李白释放了一技能。\n"; }
void skill2() { cout << "李白释放了二技能。\n"; }
void ukill() { cout << "李白释放了大绝招。\n"; }
};
int main()
{
//根据用户选择的英雄,施展一技能、二技能和大绝招
int id = 0; //英雄的id
cout << "请输入英雄(1-西施、2-韩信、3-李白):";
cin >> id;
//创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数
Hero* ptr = nullptr;
if (id == 1) { //1-西施
ptr=new XS;
}
else if (id == 2) { //2-韩信
ptr = new HX;
}
else if (id == 3) { //3-李白
ptr = new LB;
}
if (ptr != nullptr) {
ptr->skill1();
ptr->skill2();
ptr->ukill();
delete ptr;
}
}
19.3多态的对象模型
类的普通成员函数的地址是静态的,在编译阶段已指定
如果基类中有虚函数,对象的内存模型中有一个虚函数表,表中存放了基类的函数名和地址
如果派生类中重定义了基类的虚函数,创建派生类对象时,将用派生类的函数取代虚函数表中基类的函数
C++中的多态分为两种:静态多态、动态多态
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题
*19.4如何析构派生类
构造函数不能继承,创建派生类对象时,先执行基类构造函数,再执行派生类构造函数
析构函数不能继承,而销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数
如果手工的调用派生类的析构函数,也会自动调用基类的析构函数
示例:
class AA //基类
{
public:
AA() { cout << "调用了基类的构造函数AA()。\n"; }
virtual void func() { cout << "调用了基类的func()。\n"; }
//加virtual关键字,设置为虚构函数
virtual ~AA() { cout << "调用了基类的析构函数~AA()。\n"; }
};
class BB:public AA //派生类
{
public:
BB() { cout << "调用了基类的构造函数BB()。\n"; }
void func() { cout << "调用了派生类的func()。\n"; }
~BB() { cout << "调用了基类的析构函数~BB()。\n"; }
};
int main()
{
BB *b=new BB;
////C++编译器强制规定,派生类的析构函数再执行完后,会自动执行基类的析构函数
//b->~BB();
//b->~BB();
//b->~BB();
delete b;
}
析构派生类的要点如下:
1)析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉
2)析构函数可以手动调用,如果对象中有堆内存,析构函数中以下代码是必要的:
delete ptr;
ptr=nullptr;
3)用基类指针指向派生类对象时,delete基类指针调用的是基类的析构函数,不是派生类的,如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数
4)C++编译器对虚析构函数做了特别的处理
5)对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数
6)赋值运算符函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特征标随类而异,它包含了一个类型为其所属类的形参
7)友元函数不是类成员,不能继承
19.5纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在某些情况下,基类中不能对虚函数给出有意义的实现,把它声明为纯虚函数
纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给该派生类去做
语法:virtual 返回值类型 函数名(参数列表)=0;
纯虚函数在基类中为派生类保留一个函数的名字,以便派生类它进行重定义。如果在基类中没有保留函数名字,则无法支持多态性
含有纯虚函数的类被称为抽象类,不能实例化对象,可以创建指针和引用
派生类必须重定义抽象类中的纯虚函数,否则也属于抽象类
基类中的纯虚析构函数也需要实现
有些时候想使一个类成为抽象类,但刚好又没有任何纯虚函数,怎么办?
方法:在想要成为抽象类的类里声明一个纯虚析构函数
20.运行阶段类型识别dynamic_cast
运行阶段类型识别(RTTI RunTime Type ldentification)为程序在运行阶段确定对象的类型,只适用于包含虚函数的类
基类指针可以指向派生类对象,如何知道基类指针指向的是哪种派生类的对象呢?(想调用派生类中的非虚函数)
dynamic_cast运算符使用指向基类的指针来生成派生类的指针,它不能回答"指针指向的是什么类的对象"的问题,但能回答"是否可以安全的将对象的地址赋给特定类型的指针"的问题
语法:派生类指针=dynamic_cast<派生类类型*>(基类指针);
如果转换成功,dynamic_cast返回对象的地址,如果失败,返回nullptr
注意:
- dynamic_cast可以将派生类指针转换为基类指针,这种做法没有意义
- dynamic_cast可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出现bad_cast异常
示例:
class Hero //英雄基类
{
public:
int viability; //生存能力
int attack; //攻击伤害
virtual void skill1() { cout << "英雄释放了一技能。\n"; }
virtual void skill2() { cout << "英雄释放了二技能。\n"; }
virtual void ukill() { cout << "英雄释放了大绝招。\n"; }
};
class XS :public Hero //西施派生类
{
public:
void skill1() { cout << "西施释放了一技能。\n"; }
void skill2() { cout << "西施释放了二技能。\n"; }
void ukill() { cout << "西施释放了大绝招。\n"; }
void show() { cout << "我是天下第一美女.\n"; }
};
class HX :public Hero //韩信派生类
{
public:
void skill1() { cout << "韩信释放了一技能。\n"; }
void skill2() { cout << "韩信释放了二技能。\n"; }
void ukill() { cout << "韩信释放了大绝招。\n"; }
};
class LB :public Hero //李白派生类
{
public:
void skill1() { cout << "李白释放了一技能。\n"; }
void skill2() { cout << "李白释放了二技能。\n"; }
void ukill() { cout << "李白释放了大绝招。\n"; }
};
int main()
{
//根据用户选择的英雄,施展一技能、二技能和大绝招
int id = 0; //英雄的id
cout << "请输入英雄(1-西施、2-韩信、3-李白):";
cin >> id;
//创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数
Hero* ptr = nullptr;
if (id == 1) { //1-西施
ptr = new XS;
}
else if (id == 2) { //2-韩信
ptr = new HX;
}
else if (id == 3) { //3-李白
ptr = new LB;
}
if (ptr != nullptr) {
ptr->skill1();
ptr->skill2();
ptr->ukill();
////如果基类指针指向的对象是西施,那么就调用西施的show()函数
//if (id == 1) {
// XS* pxs = (XS*)ptr; //C风格强制转换的方法,程序员必须保证目标类型正确
// pxs->show();
//}
XS* xsptr = dynamic_cast<XS*>(ptr); //把基类指针转换为派生类
if (xsptr != nullptr)xsptr->show(); //如果转换成功,调用派生类西施的非虚函数
delete ptr;
}
}
21.typeid运算符和type_info类
typeid运算符用于获取数据类型的信息
语法一:typeid(数据类型);
语法二:typeid(变量名或表达式);
示例:
class AA{ //定义一个类
public:
AA(){}
};
int main()
{
//typeid用于C++内置的数据类型
int i = 3;
int* pi = &i;
int& ri = i;
cout << "typeid(int)=" << typeid(int).name() << endl;
cout << "typeid(i)=" << typeid(i).name() << endl;
cout << "typeid(int*)=" << typeid(int*).name() << endl;
cout << "typeid(pi)=" << typeid(pi).name() << endl;
cout << "typeid(int&)=" << typeid(int&).name() << endl;
cout << "typeid(ri)=" << typeid(ri).name() << endl;
return 0;
//typeid用于自定义的数据类型
AA aa;
AA*paa=&aa;
AA& raa = aa;
cout << "typeid(AA)=" << typeid(AA).name() << endl;
cout << "typeid(aa)=" << typeid(aa).name() << endl;
cout << "typeid(AA*)=" << typeid(AA*).name() << endl;
cout << "typeid(paa)=" << typeid(paa).name() << endl;
cout << "typeid(int&)=" << typeid(int&).name() << endl;
cout << "typeid(ri)=" << typeid(ri).name() << endl;
}
typeid运算符返回type_info类(在头文件< typeinfo >中定义)的对象的引用
type_info类的实现岁编译器而异,但至少有name()成员函数,该函数返回一个字符串,通常是类名
type_info重载了==和!=运算符,用于对类型进行比较
注意:
- type_info的构造函数是private属性,也没有拷贝构造函数,所以不能直接实例化,只能由编译器在内部实例化
- 不建议用name()成员函数返回的字符串作为判断数据类型的依据(编译器可能会转换类型名)
- typeid运算符可以用于多态的场景,在运行阶段识别对象的数据类型
- 假设有表达式typeid(*ptr),当ptr是空指针时,如果ptr是多态的类型,将引发bad_typeid异常
二、C++泛编程
- 自动推导类型
- 函数模板
- 类模板
1.自动推导类型
在C语言和C++98中,auto关键字用于修饰变量(自动存储的局部变量)
在C++11中,赋予了auto全新的含义,不再用于修饰的变量,而是作为一个类型指示符,指示编译器在编译时推导auto声明的变量的数据类型
在Linux平台下,编译需要加-std=C++11参数
示例:
string func()
{
return"西施";
}
int main()
{
//auto可直接用作通用数据类型(但不推荐使用),把鼠标放在变量abc上可显示出来
auto a = 3+1; cout << "a=" << a << endl;
auto b = 3.6+a; cout << "b=" << b << endl;
auto c = "西施"; cout << "c=" << c << endl;
auto d=func(); cout<<"d="<<c<<endl;
}
注意:
- auto变量必须在定义是初始化
- 右值可以是具体的数值,也可以是表达式和函数的返回值等
- auto不能作为函数的形参类型
- auto不能直接声明数组
- auto不能定义类的非静态成员变量
不要滥用auto,在编程时真正的用途如下:
- 代替冗长复杂的变量声明
示例:
double func(double b, const char* c, float d, short e, long f)
{
cout << ",b=" << b << ",c=" << c << ",d=" << d << ",e" << e << endl;
return 5.5;
}
int main()
{
//声明函数指针pf
//普通方法
double(*pf)(double, const char*, float, short, long);
pf = func;
pf(2, "西施", 3, 4, 5);
//auto方法
auto pf1 = func;
pf1(2, "西施", 3, 4, 5);
}
- 在模板中,用于声明依赖模板参数的变量
- 函数模板依赖模板参数的返回值
- 用于lambda表达式中
2.函数模板
2.1函数模板的基本概念
函数模板是通用的函数描述,使用任意类型(泛型)
编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成该类型的函数定义
生成函数定义的过程被称为实例化
创建交换两个变量的函数模板:
template <typename anytype> //anytype可用T代替
void Swap(anytype &a,anytype &b)
{
anytype tmp;
tmp=a;
a=b;
b=tmp;
}
在C++98添加关键字typename之前,C++使用关键字class来创建模板
示例:
template <typename T> //anytype可用T代替
void Swap(T& a, T& b) //交换两个变量的值的函数模板
{
T tmp;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 30; //string a="西施",b="西瓜";
//手动推导
//Swap<string>(a, b);
//自动推导
Swap(a, b);
cout << "a=" << a << ",b=" << b << endl;
}
注意事项:
- 可以为类的成员函数创建模板,但不能是虚函数和析构函数
示例:
class CGirl
{
public:
template<typename T>
CGirl(T a)
{
cout << "a=" << a << endl;
}
template<typename T>
void show(T b)
{
cout << "b=" << b << endl;
}
};
int main()
{
CGirl g("西施");
g.show(3);
g.show("西施");
}
- 使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上
示例:
template<typename T>
void Swap()
{
cout << "调用了Swap()函数。\n";
}
int main()
{
Swap<char>(); //<>里一定要有具体的数据类型
}
- 使用函数模板时,推导的数据类型必须适应函数模板中的代码
- 使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换
示例:
template<typename T>
T Add(T a,T b)
{
return a+b;
}
int main()
{
int a = 10;
char b = 20;
int c = Add<int>(a, b); //如果显式指定了函数模板的数据类型,可以发生隐式类型转换
cout << "c=" << c << endl;
}
- 函数模板支持多个通用数据类型的参数
示例:
template<typename T> //1个通用参数的函数模板
T Add(T a,T b)
{
return a+b;
}
template<typename T1,typename T2> //2个通用参数的函数模板
void show(T1 no, T2 message)
{
cout << "亲爱的" << no << "号:" << message << endl;
}
int main()
{
int a = 10;
char b = 20;
int c = Add<int>(a, b);
cout << "c=" << c << endl;
int bh = 3; //超女编号
string message = "我是好人";
show(bh, message);
}
- 函数模板支持重载,可以有非通用数据类型的参数
示例:
template<typename T>
void func(T a)
{
cout << "func(T a)\n";
}
template<typename T1,typename T2> //创建两个函数模板
void func(T1 a, T2 b)
{
cout << "func(T1 a,T2 b)\n";
}
template<typename T1, typename T2> //创建两个函数模板
void func(T1 a, T2 b,int c)
{
cout << "func(T1 a,T2 b,int c)\n";
}
int main()
{
func(1);
func(1, 2);
func(1, 2, 3);
}
2.2函数模板的具体化
可以提供一个具体化的函数的定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板
具体化(特例化、特化)的语法:
template<>void 函数模板名<数据类型>(参数列表)
template<>void 函数模板名(参数列表)
{
//函数体
}
示例:
class CGirl //类
{
public:
int m_bh; //编号
string m_name; //姓名
int m_rank; //排名
};
template<typename T>
void Swap(T& a, T& b) //交换两个变量的值函数模板
{
T tmp = a;
a = b;
b = tmp;
cout << "调用了Swap(T& a,T&b)\n";
}
template<>void Swap<CGirl>(CGirl& g1, CGirl& g2) //交换两个超女对象的排名
//template<>void Swap(CGirl& g1, CGirl& g2) //交换两个超女对象的排名
{
int tmp = g1.m_rank;
g1.m_rank = g2.m_rank;
g2.m_rank = tmp;
cout << "调用了Swap(CGirl& g1, CGirl& g2)\n";
}
int main()
{
int a = 10, b = 20;
Swap(a, b); //使用了函数模板
cout << "a=" << a << ",b=" << b << endl;
CGirl g1, g2;
g1.m_rank = 1; g2.m_rank = 2;
Swap(g1, g2); //使用了超女类的具体化函数
cout << "g.m_rank=" << g1.m_rank << ",g2.m_rank=" << g2.m_rank << endl;
}
对于给定的函数名,可以有普通函数、函数模板和具体化的函数模板,以及它们的重载版本
编译器使用各种函数的规则:
- 具体化优先于常规模板,普通函数优先于具体化和常规模板
- 如果希望使用函数模板,可以用空模板参数强制使用函数模板
- 如果函数模板能产生更好的匹配,将优先于非模板函数
示例:
void Swap(int a, int b) //普通函数
{
cout << "使用了普通函数。\n";
}
template<typename T>
void Swap(T a, T b) //函数模板
{
cout << "使用了函数模板。\n";
}
template<>
void Swap(int a, int b) //函数模板的具体化版本
{
cout << "使用了具体化的函数模板。\n";
}
int main()
{
Swap('c', 'd');
}
2.3函数模板分文件编写
- 普通函数的声明放在头文件中,定义放在源文件中
- 函数模板只是函数的描述,没有实体
- 《C++ Primer plus》:函数模板一般放在头文件中
函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中
函数模板的具体化有实体。编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中
示例:
//demo01.cpp
#include"public.h"
int main()
{
Swap('c', 'd'); //将使用函数模板
Swap(1, 1); //将使用普通函数
Swap<>(1, 1); //将使用函数模板的具体化版本
}
//public.cpp
#include"public.h"
void Swap(int a, int b) //普通函数
{
cout << "使用了普通函数。\n";
}
template<>
void Swap(int a, int b) //函数模板的具体化版本
{
cout << "使用了具体化的函数模板。\n";
}
//public.h
#pragma once
#include<iostream>
using namespace std;
void Swap(int a, int b); //普通函数
template<typename T>
void Swap(T a, T b) //函数模板
{
cout << "使用了函数模板。\n";
}
template<>
void Swap(int a, int b);
2.4函数模板高级
1)decltype关键字
在C++11中,decltype操作符,用与查询查询表达式的数据类型
语法:decltype(expression) var;
decltype分析表达式并得到它的类型,不会计算执行表达式。函数调用也一种表达式,因此不必担心在使用decltype时执行了函数
decltype推导规则(按步骤):
- 如果expression是一个没有用括号起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符
示例:
int main()
{
short a=5;
short b=10;
short& ra=a;
decltype(ra)da=b;
}
- 如果expression是一个函数调用,则var的类型与函数的返回值类型相同(函数不能返回void,但可以返回void*)
示例:
int func()
{
cout << "调用了func()函数。\n";
return 3;
}
int main()
{
decltype(func)* da = func;
da();
}
- 如果expression是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型是expression的引用
示例:
int func()
{
cout << "调用了func()函数。\n";
return 3;
}
int main()
{
decltype((func) da = func;
da();
}
- 如果上面的条件都不满足,则var的类型与expression的类型相同
如果需要多次使用decltype,可以结合typede和suing
2)函数后置返回类型
int func(int x,double y);
等同:
auto func(int x,double y) -> int;
将返回类型移到了函数声明的后面
auto是占位符(C++11给auto新增的角色),为函数返回值占了一个位置
这种语法也可以用于函数定义:
auto func(int x,double y) -> int
{
//函数体
}
示例:
template<typename T1,typename T2>
auto func(T1 x,T2 y)->decltype(x+y)
{
decltype(x+y) tmp = x + y;
cout << "tmp="<<tmp<<endl;
return tmp;
}
int main()
{
func(3, 5.8);
}
3)C++14的auto关键字
C++14标准对函数返回类型推导规则做了优化,函数的返回值可以用auto,不必尾随返回类型
示例:
template<typename T1,typename T2>
auto func(T1 x,T2 y)
{
decltype(x+y) tmp = x + y;
cout << "tmp="<<tmp<<endl;
return tmp;
}
int main()
{
func(3, 5.8);
}
3.模板类
3.1模板类的基本概念
类模板是通用类的描述,使用任意类型(泛型)来描述类的定义
使用类模板的时候,指定具体的数据类型,让编译器生成该类型的类定义
语法:
template < class T >
class 类模板名
{
类的定义;
};
函数模板建议用typename描述通用数据类型,类模板建议用class
示例:
template<class T1,class T2>
class AA
{
public:
T1 m_a; //通用类型用于成员变量
T2 m_b; //通用类型用于成员变量
AA(){} //默认构造函数是空的
//通用类型用于成员函数的参数
AA(T1 a,T2 b):m_a(a),m_b(b){}
//通用类型用于成员函数的返回值
T1 geta() //获取成员m_a的值
{
T1 a = 2; //通用类型用于成员函数的代码中
return m_a + a;
}
T2 getb() //获取成员m_b的值
{
T2 b = 1; //通用类型用于成员函数的代码中
return m_b + b;
}
};
int main()
{
//用模板类AA创建对象a
AA<int, double>a; //告诉编译器,用int和double取代类模板中的T1和T2
a.m_a = 20; a.m_b = 30;
cout << "a.geta()=" << a.geta() << endl;
cout << "a.getb()=" << a.getb() << endl;
}
注意:
- 在创建对象的时候,必须指明具体的数据类型
- 使用类模板时,数据类型必须适应类模板中的代码
- 类模板可以为通用参数指定缺省的数据类型(C++11标准的函数模板也可以)
- 类的成员函数可以在类外实现
示例:
template<class T1,class T2>
class AA
{
public:
T1 m_a; //通用类型用于成员变量
T2 m_b; //通用类型用于成员变量
AA(){} //默认构造函数是空的
//通用类型用于成员函数的参数
AA(T1 a,T2 b):m_a(a),m_b(b){}
//通用类型用于成员函数的返回值
T1 geta() //获取成员m_a的值
{
T1 a = 2; //通用类型用于成员函数的代码中
return m_a + a;
}
T2 getb();
};
template<class T1, class T2 >
T2 AA<T1, T2>::getb()
{
return m_b;
}
int main()
{
//用模板类AA创建对象a
AA<int, double>a; //告诉编译器,用int和double取代类模板中的T1和T2
a.m_a = 20; a.m_b = 30;
cout << "a.geta()=" << a.geta() << endl;
cout << "a.getb()=" << a.getb() << endl;
}
- 可以用new创建模板对象
3.2模板类的示例--栈
- 模板类最常用的就是作为容器类
- C++标准库:栈、数组、链表、二叉树、哈希表
创建模板类的方法:
- 先写一个普通类,用具体的数据类型
- 调式普通类,确定没问题
- 把普通类改为模板类
示例:
//定义栈元素的数据类型。
//此行代码的好处是当需要改数据类型时,只需在此行更改数据类型
//typedef int DataType; 下文中的DataType是替换了int
template<class DataType>
class Stack //栈类
{
private:
DataType* items; //栈数组
int stacksize; //栈实际的大小
int top; //栈顶指针
public:
//构造函数:1-分配栈数组内存;2-把栈顶指针初始化为0
Stack(int size) :stacksize(size), top(0) {
items = new DataType[stacksize];
}
~Stack() {
delete items; items = nullptr;
}
bool isempty()const { //判断栈是否为空
return top == 0;
}
bool isfull()const { //判断栈是否已满
return top == stacksize;
}
bool push(const int& item) { //元素入栈
if (top < stacksize) { items[top++] = item; return true; }
return false;
}
bool pop(int& item) { //元素出栈
if (top>0) { item=items[--top] ; return true; }
return false;
}
};
int main()
{
Stack<int> ss(5); //创建栈对象,大小是5
//元素入栈
ss.push(1); ss.push(2); ss.push(3); ss.push(4); ss.push(5);
//元素出栈
int item;
while (ss.isempty() == false)
{
ss.pop(item); cout << "item=" << item << endl;
}
}
3.3模板类的示例--数组
- 定长数组:array容器(C++11标准)
- 可变数组:vector容器
- 类模板的非通用类型参数
示例1:
//定长数组容器
template<class T,int len=10> //添加int len=10可解决数组大小问题
class Array
{
private:
T items[len]; //数组元素
public:
Array() { memset(items,0, sizeof(items)); } //默认构造函数
~Array(){} //析构函数
T& operator[](int i) { return items[i];} //重载操作符[],可以修改数组中的元素
const T& operator[](int i) const{ return items[i]; } //重载操作符[],不能修改数组中的元素
};
int main()
{
Array<int>a; //创建模板类Array的对象
a[0] = 5; a[1] = 4; a[2] = 3; a[3] =2 ; a[4] = 1;
for (int i = 0; i < 5; i++)cout << "a[" << i << "]=" << a[i] << endl;
}
示例2:
//变长数组容器
template<class T,int len=10> //添加int len=10可解决数组大小问题
class Array
{
private:
T items[len]; //数组元素
public:
Array() { memset(items,0, sizeof(items)); } //默认构造函数
~Array(){} //析构函数
T& operator[](int i) { return items[i];} //重载操作符[],可以修改数组中的元素
const T& operator[](int i) const{ return items[i]; } //重载操作符[],不能修改数组中的元素
};
template<class T> //添加int len=10可解决数组大小问题
class Vector
{
private:
int len; //数组元素的个数
T* items; //数组元素
public:
//默认构造函数,分配内存
Vector(int size=10):len(size) {
items = new T[len];
}
//析构函数
~Vector() {
delete[]items; items = nullptr;
}
//扩展数组的内存空间
void resize(int size) {
if (size <= len)return; //只能往更大扩展
T* tmp = new T[size]; //分配更大的内存空间
for (int i = 0; i < len; i++)tmp[i] = items[i]; //把原来数组中的元素复制到新数组
delete[]items; //释放原来的数组
items = tmp; //让数组指针指向新数组
len = size; //扩展后的数组长度
}
int size()const { return len; } //获取数组长度
//重载操作符[],可以修改数组中的元素
T& operator[](int i) {
if (i >= len)resize(i + 10); //扩展数组
return items[i];
}
const T& operator[](int i) const { return items[i]; } //重载操作符[],不能修改数组中的元素
};
int main()
{
//Array<int>a; //创建模板类Array的对象
Vector<int>a(1); //创建模板类Vector的对象
a[0] = 5; a[1] = 4; a[2] = 3; a[3] =2 ; a[4] = 1;
for (int i = 0; i < 5; i++)cout << "a[" << i << "]=" << a[i] << endl;
}
类模板可以有非通用类型参数:
- 通常是整型(C++20标准可以用其它)
- 实例化模板时必须用常量表达式
- 模板中不能修改参数的值
优点:在栈上分配内存,易维护,执行速度快,合适小型数组
缺点:在程序中,不同的非通用类型参数将导致编译器生成不同的类
构造函数的方法更通用,因为数组的大小是类的成员(而不是硬编码),可以创建数组大小可变的类
3.4嵌套和递归使用模板类
- 容器中有容器
- 数组的元素可以是栈
- 栈中的元素可以是数组
在C++11之前,嵌套使用模板类的时候,>>之间要加空格
示例:(有bug)
template<class DataType>
class Stack //栈类
{
private:
DataType* items; //栈数组
int stacksize; //栈实际的大小
int top; //栈顶指针
public:
//构造函数:1-分配栈数组内存;2-把栈顶指针初始化为0
Stack(int size) :stacksize(size), top(0) {
items = new DataType[stacksize];
}
~Stack() {
delete[] items; items = nullptr;
}
Stack& operator=(const Stack& v) //重载赋值运算符函数,实现深拷贝
{
delete[]items; //释放内存
stacksize = v.stacksize; //栈实际的大小
items = new DataType[stacksize]; //重新分配数组
for (int i = 0; i < stacksize; i++)items[i] = v.items[i]; //复制数组中的元素
top = v.top; //栈顶指针
return *this;
}
bool isempty()const { //判断栈是否为空
return top == 0;
}
bool isfull()const { //判断栈是否已满
return top == stacksize;
}
bool push(const int& item) { //元素入栈
if (top < stacksize) { items[top++] = item; return true; }
return false;
}
bool pop(DataType& item) { //元素出栈
if (top > 0) { item = items[--top]; return true; }
return false;
}
};
template<class T> //添加int len=10可解决数组大小问题
class Vector
{
private:
int len; //数组元素的个数
T* items; //数组元素
public:
//默认构造函数,分配内存
Vector(int size=2):len(size) {
items = new T[len];
}
//析构函数
~Vector() {
delete[]items; items = nullptr;
}
//扩展数组的内存空间
void resize(int size) {
if (size <= len)return; //只能往更大扩展
T* tmp = new T[size]; //分配更大的内存空间
for (int i = 0; i < len; i++)tmp[i] = items[i]; //把原来数组中的元素复制到新数组
delete[]items; //释放原来的数组
items = tmp; //让数组指针指向新数组
len = size; //扩展后的数组长度
}
int size()const { return len; } //获取数组长度
//重载操作符[],可以修改数组中的元素
T& operator[](int i) {
if (i >= len)resize(i + 10); //扩展数组
return items[i];
}
const T& operator[](int i) const { return items[i]; } //重载操作符[],不能修改数组中的元素
};
int main()
{
//Vector容器的大小缺省值是2,Stack容器的大小缺省值是3
//创建Vector容器,容器中的元素用Stack
Vector<Stack<string>>vs; //C++11之前,>>中间需要加空格
//手动的往容器中插入数据
vs[0].push("宝宝1"); vs[0].push("宝宝2"); vs[0].push("宝宝3"); //vs容器中的第0个栈
vs[1].push("宝宝3"); vs[1].push("宝宝2"); vs[1].push("宝宝1"); //vs容器中的第1个栈
vs[1].push("冰冰1"); vs[1].push("花花2"); //vs容器中的第2个栈
//用嵌套的循环,把容器中的数据显示出来
for (int i = 0; i < vs.size(); i++) //遍厉Vestor容器
{
while (vs[i].isempty() == false) //遍历Stack容器
{
string item;vs[i].pop(item);cout << "itrm=" << item << endl;
}
}
//创建Stack容器,容器中的元素用Vector<string>
Stack<Vector<string>>sv;
Vector<string>tmp; //栈的元素,临时Vector<string>容器
//第一个入栈的元素
tmp[0] = "宝宝1"; tmp[1] = "宝宝2"; sv.push(tmp);
//第二个入栈的元素
tmp[0] = "冰冰1"; tmp[1] = "冰冰2"; sv.push(tmp);
//第三个入栈的元素
tmp[0] = "冰冰1"; tmp[1] = "冰冰2"; tmp[2] = "冰冰3"; tmp[3] = "冰冰4"; sv.push(tmp);
//用嵌套的循环,把sv容器中的数据显示出来
while (sv.isempty() == false)
{
sv.pop(tmp); //出栈一个元素,放在临时容器中
for (int i = 0; i < tmp.size(); i++) //遍历临时Vector<string>容器,显示容器中每个元素的值
cout << "vt[" << i << "]=" << tmp[i] << endl;
}
//自己嵌套自己
//创建Vector容器,容器中的元素用Vector<string>
Vector<Vector<string>>vv; //递归使用模板类
vv[0][0] = "西施1"; vv[0][1] = "西施2"; vv[0][2] = "西施3";
vv[1][0] = "西瓜1"; vv[1][1] = "西瓜2";
vv[2][0] = "冰冰1"; vv[2][1] = "冰冰2"; vv[2][2] = "冰冰3";
//用嵌套的循环,把vv容器中的数据显示出来
for (int i = 0; i < vv.size(); i++)
{
for (int j = 0; j < vv[i].size(); j++)
cout << "vv[" << i << "][" << j << "]=" << vv[i][j] << endl;
}
}
3.5模板类具体化
模板类具体化(特化、特例化)有两种:完全具体化、部分具体化
语法请见示例程序:
//类模板
template<class T1,class T2 >
class AA
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "模板类:构造函数。\n"; }
void show()const;
};
template<class T1,class T2>
void AA<T1, T2>::show()const { //成员函数类外实现
cout << "类模板:x=" << m_x << ",y=" << m_y << endl;
}
////////////////////////////////////////////////////////////////////////
//类模板完全具体化--为这两个通用类型参数指定具体的数据类型
template<>
class AA<int, string> {
public :
int m_x;
string m_y;
AA(const int x, const string y) :m_x(x), m_y(y) { cout << "完全具体化:构造函数。\n"; }
void show()const;
};
void AA<int, string>::show()const { //成员函数类外实现
cout << "完全具体化:x=" << m_x << ",y=" << m_y << endl;
}
////////////////////////////////////////////////////////////
//类模板部分显示具体化--为多个模板参数的部分参数指定具体的数据类型
template<class T1>
class AA<T1, string> {
public:
T1 m_x;
string m_y;
AA(const T1 x, const string y) :m_x(x), m_y(y) { cout << "部分具体化:构造函数。\n"; }
void show()const;
};
template<class T1>
void AA<T1, string>::show()const { //成员函数类外实现
cout << "部分具体化:x=" << m_x << ",y=" << m_y << endl;
}
/////////////////////////////////////////////////////////////
int main()
{
//具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类
AA<int, string>aa(8, "我是一只傻傻鸟");
aa.show();
}
具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类
3.6模板类与继承
- 模板类继承普通类(常见)
示例:
class AA //普通类AA
{
public:
int m_a;
AA(int a) :m_a(a) { cout << "调用了AA的构造函数。\n"; }
void func1() { cout << "调用了func1()函数:m_a=" << m_a << endl;; }
};
template<class T1,class T2>
class BB:public AA //模板类BB
{
public:
T1 m_x;
T2 m_y;
BB(const T1 x, const T2 y,int a) :AA(a),m_x(x), m_y(y) { cout << "调用了BB的构造函数。\n"; }
void func2()const { cout << "调用了func2():x=" << m_x << ",y=" << m_y << endl; }
};
int main()
{
BB<int, string>bb(8, "我是一只啥啥鸟。",3);
bb.func2();
bb.func1();
}
- 普通类继承模板类的实例版本
示例:
//让普通类AA继承模板类BB的某一个实例化版本
//BB是模板类,可以实例化无数个类让AA继承
template<class T1, class T2>
class BB //模板类BB
{
public:
T1 m_x;
T2 m_y;
BB(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "调用了BB的构造函数。\n"; }
void func2()const { cout << "调用了func2():x=" << m_x << ",y=" << m_y << endl; }
};
class AA:public BB<int,string> //普通类AA
{
public:
int m_a;
AA(int a,int x,string y) :BB(x,y),m_a(a) { cout << "调用了AA的构造函数。\n"; }
void func1() { cout << "调用了func1()函数:m_a=" << m_a << endl;; }
};
int main()
{
AA aa(3,7,"我是一只啥啥鸟。");
aa.func1();
aa.func2();
}
- 普通类继承模板类(常见)
示例:
template<class T1, class T2>
class BB //模板类BB
{
public:
T1 m_x;
T2 m_y;
BB(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "调用了BB的构造函数。\n"; }
void func2()const { cout << "调用了func2():x=" << m_x << ",y=" << m_y << endl; }
};
template<class T1, class T2>
class AA:public BB<T1,T2> //普通类AA
{
public:
int m_a;
AA(int a,const T1 x,const T2 y) :BB<T1,T2>(x,y),m_a(a) { cout << "调用了AA的构造函数。\n"; }
void func1() { cout << "调用了func1()函数:m_a=" << m_a << endl;; }
};
int main()
{
AA<int,string> aa(3,7,"我是一只啥啥鸟。");
aa.func1();
aa.func2();
}
- 模板类继承模板类
示例:(有bug)
template<class T1, class T2>
class BB //模板类BB
{
public:
T1 m_x;
T2 m_y;
BB(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "调用了BB的构造函数。\n"; }
void func2()const { cout << "调用了func2():x=" << m_x << ",y=" << m_y << endl; }
};
template<class T1, class T2>
class AA:public BB<T1,T2> //普通类AA变成了模板类
{
public:
int m_a;
AA(int a,const T1 x,const T2 y) :BB<T1,T2>(x,y),m_a(a) { cout << "调用了AA的构造函数。\n"; }
void func1() { cout << "调用了func1()函数:m_a=" << m_a << endl;; }
};
template<class T,class T1, class T2>
class CC :public BB<T1, T2> //模板类继承模板类
{
public:
T m_a;
CC(conat T a, const T1 x, const T2 y) :BB<T1, T2>(x, y), m_a(a) { cout << "调用了CC的构造函数。\n"; }
void func3() { cout << "调用了func3()函数:m_a=" << m_a << endl;; }
};
int main()
{
CC<int,int,string> cc(3,8,"我是一只啥啥鸟。");
cc.func3();
cc.func2();
}
- 模板类继承模板参数给出的基类(不能是模板类)
示例:
class AA {
public:
AA() { cout << "调用了AA的构造函数AA().\n"; }
AA(int a) { cout << "调用了AA的构造函数AA().\n"; }
};
class BB {
public:
BB() { cout << "调用了AA的构造函数BB().\n"; }
BB(int a) { cout << "调用了AA的构造函数BB().\n"; }
};
class CC {
public:
CC() { cout << "调用了AA的构造函数CC().\n"; }
CC(int a) { cout << "调用了AA的构造函数CC().\n"; }
};
template<class T>
class DD {
public:
DD() { cout << "调用了DD的构造函数DD().\n"; }
DD(int a) { cout << "调用了DD的构造函数DD().\n"; }
};
template<class T>
class EE :public T{ //模板类继承模板参数给出的基类
public:
EE():T() { cout << "调用了EE的构造函数EE().\n"; }
EE(int a):T(a) { cout << "调用了EE的构造函数EE().\n"; }
};
int main()
{
EE<AA>ea1; //AA作为基类
EE<BB>eb1; //BB作为基类
EE<CC>ec1; //CC作为基类
EE<DD<int>>ed1; //DD是模板类,也可作为基类,一定要明确指出模板类DD的数据类型
//EE<DD>ed1; //DD作为基类,错误
}
3.7模板类与函数
模板类可以用作于函数的参数和返回值,有三种形式:
- 普通函数,参数和返回值是模板类的实例化版本
- 函数模板,参数和返回值是某种的模板类
- 函数模板,参数和返回值是任意类型(支持普通类和模板类和其它类型)
示例:
template<class T1,class T2>
class AA //模板类AA
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x,const T2 y):m_x(x),m_y(y){}
void show()const { cout << "show() x=" << m_x << ",y=" << m_y << endl; }
};
//采用普通函数,参数和返回值是模板类的实例化版本
AA<int,string> func(AA<int,string>&aa)
{
aa.show();
cout << "调用了func(AA<int,string>&aa)函数。\n";
return aa;
}
//函数模板,参数和返回值是模板类AA
template<typename T1,typename T2>
AA<T1, T2> func(AA<T1,T2>& aa)
{
aa.show();
cout << "调用了func(AA<T1,T2>&aa)函数。\n";
return aa;
}
//函数模板,参数和返回值是任意类型
template<typename T>
T func(T &aa)
{
aa.show();
cout << "调用了func(AA<T>&aa)函数。\n";
return aa;
}
int main()
{
AA<int, string>aa(3, "我是一只鸟。");
func(aa);
}
3.8模板类与友元
模板类的友元函数有三类:
- 非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数,只能在类内实现
示例:
//利用模板类的参数,自动生成友元函数
template<class T1,class T2>
class AA //模板类AA
{
T1 m_x;
T2 m_y;
public:
AA(const T1 x,const T2 y):m_x(x),m_y(y){}
//非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数,只能在类内实现
friend void show(const AA<T1, T2>& a)
{
cout << "x=" << a.m_x << ",y=" << a.m_y << endl;
}
};
int main()
{
AA<int, string>a(88, "我是煞笔。");
show(a);
AA<char, string>b(88, "我是你大爷。");
show(b);
}
- 约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数
示例:
//约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数
template<typename T>
void show(T& a); //第一步:在模板类的定义前面,声明友元函数模板
template<class T1,class T2>
class AA //模板类AA
{
friend void show<>(AA<T1, T2>& a); //第二步:在模板类中,再次声明友元函数模板
T1 m_x;
T2 m_y;
public:
AA(const T1 x,const T2 y):m_x(x),m_y(y){}
};
template<class T1, class T2>
class BB //模板类BB
{
friend void show<>(BB<T1, T2>& a); //第二步:在模板类中,再次声明友元函数模板
T1 m_x;
T2 m_y;
public:
BB(const T1 x, const T2 y) :m_x(x), m_y(y) {}
};
//这种友元的函数模板可以用于多个模板类
template <typename T> //第三步,友元函数模板的定义
void show(T& a)
{
cout << "通用:x=" << a.m_x << ",y=" << a.m_y << endl;
}
template <> //第四步:具体化版本
void show(AA<int,string>& a)
{
cout << "具体<int,string>:x=" << a.m_x << ",y=" << a.m_y << endl;
}
template <> //第四步:具体化版本
void show(BB<int, string>& a)
{
cout << "具体BB<int,string>:x=" << a.m_x << ",y=" << a.m_y << endl;
}
int main()
{
AA<int, string>a1(88, "我是煞笔。");
show(a1); //将使用具体化的版本
AA<char, string>a2(88, "我是你大爷。");
show(a2); //将使用通用的版本
BB<int, string>b1(88, "我是好人。");
show(b1); //将使用具体化的版本
BB<char, string>b2(88, "今天天气真好。");
show(b2); //将使用通用的版本
}
- 非约束模板友元:模板类实例化时,如果实例化了n个类,也会实例化n个友元函数,每个实例化的类都拥有n个友元函数(这种友元也模板函数,语法比较简单,但,在实际开发中一般不用,因为它不科学)
3.9模板类的成员模板
示例:
template<class T1,class T2>
class AA //模板类AA
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x,const T2 y):m_x(x),m_y(y){}
void show() { cout << "m_x=" << m_x << ",m_y=" << m_y << endl; }
template<class T>
class BB
{
public:
T m_a;
T1 m_b;
BB(){}
void show(); //只保留声明
};
BB<string>m_bb; //对象bb是类模板AA的一个成员
template<typename T> //这个T和上面的T没关系
void show(T tt);
};
//模板类中的成员模板,定义的代码可以写在类的外面,上面只保留声明,如下:
template<class T1, class T2>
template<typename T>
void AA<T1,T2>::BB<T>::show(){
cout << "m_a=" << m_a << ",m_b=" << m_b << endl;
}
template<class T1, class T2>
template<class T>
void AA<T1,T2>::show(T tt) {
cout << "t=" << tt << endl;
cout << "m_x=" << m_x << ",m_y=" << m_y << endl;
m_bb.show();
}
int main()
{
AA<int, string>a(88, "我是煞笔。");
a.show();
a.m_bb.m_a = "我是好人";
a.m_bb.show();
a.show("你是鬼");
}
3.10将模板类用作参数
示例:(有bug)
//在类中,用T定义数组指针,len指定数组的长度
//C++支持模板的模板
//这个demo程序有两种线性表:链表和数组
//以下是模板的模板代码
template<class T1,class len> //T1(通用类型参数)-数据类型;len(非通用类型参数)-链表长度
class LinkList //链表模板类
{
public:
T1* m_head; //链表头结点
int m_len = len; //表长
void insert() { cout << "向链表中插入了一条记录。\n"; }
void ddelete() { cout << "向链表中删除了一条记录。\n"; }
void update() { cout << "向链表中更新了一条记录。\n"; }
};
template<class T1, int len>
class Array //数组类模板
{
public:
T1* m_data; //数组指针
int m_len = len; //表长
void insert() { cout << "向链表中插入了一条记录。\n"; }
void ddelete() { cout << "向链表中删除了一条记录。\n"; }
void update() { cout << "向链表中更新了一条记录。\n"; }
};
//线性表模板类:tabletype-线性表类型,datatype-线性表的数据类型
template<template<class, int>class T1, class T2, int len>
class LinearList
{
public:
T1<T2, len>m_table; //创建线性表对象
void insert() { m_table.insert(); } //线性表插入操作
void ddelete() { m_table.ddelete(); } //线性表删除操作
void update() { m_table.update(); } //线性表更新操作
void oper() //按业务要求操作线性表
{
cout << "len=" << m_table.m_len << endl;
m_table.insert();
m_table.update();
}
};
int main()
{
//创建线性表对象,容器类型为数组,数组的数据类型为string,表长为20
LinearList<LinkList, int, 20>a; // <模板名,指定容器的数据类型,指定容器的大小>
//操作容器的方法
a.insert();
a.ddelete();
a.update();
//创建线性表对象,容器类型为数组,数组的数据类型为string,表长为20
LinearList<Array, string, 20>b;
b.insert();
b.ddelete();
b.update();
}
4.编译
4.1编译预处理
C++程序编译的过程:预处理->编译(优化、汇编)->链接
预处理指令主要有以下三种:
- 包含头文件:#include
- 宏定义:#define(定义宏)、#undef(删除宏)
- 条件编译:#ifdef、#ifndef
1)包含头文件
include包含头文件有两种方式:
- #include<文件名>:也叫系统头文件,直接从编译器自带的函数库目录中寻找文件
- #include"文件名":先从自定义的目录中寻找文件,如果找不到,再从编译器自带的函数库目录中寻找
include也包含其它的文件,如:.h 、 .cpp或其它的文件**
C++98标准后的头文件:
- C的标准库,老版本的有.h后缀,新版本没有。增加了字符c的前缀,例如:老版本是<stdio.h>,新版本是< cstdio >,库中的内容是一样的,新老版本都可用。在程序中,不指定std命名空间也能使用库中的内容
- C++的标准库:老版本的有.h后缀;新版本没有。例如:老版本是<iostream.h>,新版本是< iostream >,老版本已弃用,只能用新版本。在程序中,必须指定std命名空间才能使用库中的内容
注意:用户自定义的头文件还是用.h为后缀
2)宏定义指令
无参数的宏:#define 宏名 宏内容
有参数的宏:#define MAX (x,y) ((x,y)?(x):(y))
示例:
//不要把宏内容当成数据,宏内容没有数据类型的说法,只是符号
//可以用const修饰的变量代替宏,C++也建议这样做
#define BH 3
#define MESSAGE "我是好人。"
int main()
{
cout << "亲爱的" << 3 << "号:" << "我是坏人。"; //"我是坏人"中的""也宏的一部分
}
编译的时候,编译器把程序中的宏名用宏内容替换,是为宏展开(宏替换)
宏可以只有宏名,没有后面的宏内容
在C++中,内联函数可代替有参数的宏,效果更好
C++中常用的宏:
- 当前源代码文件名:__ FILE __
- 当前源代码函数名:__ FUNCTION__
- 当前源代码行号:__ LINE __
- 编译的日期:__ DATE __
- 编译的时间:__ TIME __
- 系统时间戳:__ TIMESTAMP __
- 当用C++编译程序时,宏__cplusplus就会被定义
示例:
int main()
{
cout << "当前源代码文件名:__FILE__=" << __FILE__ << endl;
cout << "当前源代码函数名:__ FUNCTION__=" << __FUNCDNAME__ << endl;
cout << "当前源代码行号:__ LINE__=" << __LINE__ << endl;
cout << "编译的日期:__ DATE __=" << __DATE__ << endl;
cout << "编译的时间:__ TIME __=" << __TIME__ << endl;
cout << "当前系统时间戳:__TIMESTAMP__=" << __TIMESTAMP__ << endl;
cout << "_cplusplus=" << __cplusplus << endl;
}
3)条件编译
最常用的两种:#ifdef、#ifndef
#ifdef 宏名
程序段—
#else
程序段二
#endif
含义:如果#ifdef后面的宏名已存在,则使用程序段一,否则使用程序段二
示例:
//假设有一个需求,要写一个跨平台的C++程序
//c++虽然有标准委员会,但,不同的平台,不同的编译器,实现是有区别的
//在VS中,long是32位,long long 才是64位
//在Linux中,long是64位
//不同的操作系统会定义不同的宏
int main()
{
#ifdef _WIN32
cout << "这是Windows系统。\n";
typedef long long int64;
#else
cout << "这不是Windows系统。\n";
typedef long int64;
#endif
int64 a = 10;
cout << "a=" << a;
}
#ifndef 标识符
程序段一
#else
程序段二
#endif
含义:如果#ifndef后面的宏名不存在,则使用程序段一,否则使用程序段二
4)解决头文件中代码重复包含的问题
在C/C++中,在使用预编译指令#include的时候,为了防止头文件被重复包含,有两种方式
第一种:用#ifndef指令
#ifndef _ GIRL _
#define _ GIRL _
//代码内容
#endif
第二种:把#pragma once指令放在文件的开头
ifndef方式守C/C++语言标准的支持,不受编译器的任何限制;而#pragma once 方式有些编译器不支持
ifndef可以针对文件中的部分代码;而#pragma once只能针对整个文件
4.2编译和链接
一、源代码的组织
头文件(*.h):#include头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、#define和const定义的常量等
源文件(*.cpp):函数的定义、类的定义、模板具体化的定义
主程序(main函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include包含进来
示例:
//demo01.cpp
#include"tools.h"
#include"girls.h"
int main()
{
cout << "max(5,8)=" << max(5, 8) << endl;
cout << "min(5,8)=" << min(5, 8) << endl;
print(3, "我是好人。");
}
//tools.cpp
#include"tools.h"
int max(int a, int b) //比较大小,返回最大值
{
return a > b ? a : b;
}
int min(int a, int b) //返回最小值
{
return a < b ? a : b;
}
//girls.cpp
#include"girls.h"
void print(int no, string str) //表白
{
cout << "亲爱的" << no << "号:" << str << endl;
}
//tools.h
#pragma once
#include<iostream>
#include"girls.h"
using namespace std;
int max(int a, int b); //比较大小,返回最大值
int min(int a, int b); //返回最小值
//girls.h
#pragma once
#include<iostream>
#include"tools.h"
using namespace std;
void print(int no, string str); //表白神器
二、编译预处理
预处理的包括以下方面:
- 处理#include头文件包含指令
- 处理#ifdef #else #endif、#ifndef #else #endif条件编译指令
- 处理#define宏定义
- 为代码添加行号、文件名和函数名
- 删除注释
- 保留部分#pragma编译器指令(编译的时候会用到)
三、编译
将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制文件)
四、链接
将编译后的目标文件,以及他们所需的库文件链接在一起,形成一个整体
五、更多细节
- 分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高
- 编译单个*.cpp文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误
- 编译单个*.cpp文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译
- 如果函数和类的定义不存在,编译不会报错,但连接会出现无法解析的外部命令
- 链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误(如果把变量、函数和类的定义放在 *.h 文件中, *.h 会被多次包含,连接前会存在多个副本,如果放在 .cpp文件中,.cpp文件不会被包含,只会被编译一次,链接前只存在一个版本)
- 把变量、函数和类的定义放在*.h中是不规范的做法,如果 *.h被多个 *.cpp包含,会出现重定义
- 用#include包含 *.cpp 是不规范的做法,原理同上
- 尽可能不使用全局变量,如果一定要用,要再在 *.h文件中声明(需要加extern), *.cpp中定义
- 全局的const常量在头文件中和定义(const常量仅在文件内有效)
- *.h 重复包含的处理方法只对单个的 *.cpp 文件有效,不是整个项目
- 函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在 *.h文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在 *.cpp文件中
- Linux下C++编译和链接的原理与VS一样
5.命名空间
在实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字冲突的情况
命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突
一、语法:
创建命名空间:
namespace 命名空间的名字
{
//类、函数、模板、变量的声明和定义
}
创建命名空间的别名:
namespace 别名=原名
二、使用命名空间
在同一命名空间内的名字可以直接访问,该命名空间之外的代码则必须明确指出命名空间
1)运算符::
语法:命名空间::名字
简单明了,而且不会造成任何冲突,但使用起来比较繁琐
2)using声明
语法:using 命名空间::名字
用using声明后,就可以进行直接使用名称了
注意:如果该声明区域已存在相同的名字,会报错
3)suing编译指令
语法:using namespace 命名空间
using编译指令将整个命名空间中的名字可用。
注意:如果声明区域有相同的名字,局部版本将隐藏命名空间中的名字,不过,可以使用域名解析符使用命名空间中的名称
示例:
namespace AA {
int ab = 1; //全局变量
void func1(); //全局函数的声明
class A1 //类
{
public:
void show(); //类的成员函数
};
void func1() { //全局函数的定义
cout << "调用了func()函数。\n";
}
void A1::show() { //类成员函数的类外实现
cout << "调用了A1::show()函数。\n";
}
}
//使用命名空间的方法有3种
int main()
{
/*cout << "ab=" << ab << endl;
func1();
A1 a;
a.show();*/
////1、在名字前面加上命名空间的名字和两个冒号
//cout << "ab=" << AA::ab << endl;
//AA::func1();
//AA::A1 a;
//a.show();
////2、用using声明。注意:如果在main中已存在相同的名字,会报错
//using AA::ab;
//using AA::A1;
//using AA::func1;
//cout << "ab=" << ab << endl;
//func1();
//A1 a;
//a.show();
//3、用using编译指令。
// 注意:如果在main中已存在相同的名字,编译不会报错,但会屏蔽命名空间中的变量,需在变量前面加"命名空间的名称::"
using namespace AA;
cout << "ab=" << ab << endl;
func1();
A1 a;
a.show();
}
当有多个命名空间时
示例:
namespace AA {
int ab = 1; //全局变量
void func1(); //全局函数的声明
class A1 //类
{
public:
void show(); //类的成员函数
};
void func1() { //全局函数的定义
cout << "调用了func()函数。\n";
}
void A1::show() { //类成员函数的类外实现
cout << "调用了A1::show()函数。\n";
}
}
namespace BB {
int ab = 3; //全局变量
void func1(); //全局函数的声明
class A1 //类
{
public:
void show(); //类的成员函数
};
void func1() { //全局函数的定义
cout << "调用了BB::func()函数。\n";
}
void A1::show() { //类成员函数的类外实现
cout << "调用了BB::A1::show()函数。\n";
}
}
int main()
{
using namespace AA;
using namespace BB;
cout << "AA::ab=" << AA::ab << endl;
AA::func1();
AA::A1 a;
a.show();
cout << "BB::ab=" << BB::ab << endl;
}
四、注意事项
- 命名空间时全局的,可以分布在多个文件中
- 命名空间可以嵌套
- 在命名空间中声明全局变量,而不是使用外部全局变量和静态变量
- 对于using声明,首选将器作用域设置为局部而不是全局
- 不要在头文件中使用using编译指令,如果非要使用,应将它放在所有的#include之后
- 匿名的命名空间,从创建的位置到文件结束有效
6.C++类型转换
6.1 static_cast
C风格的类型转换很容易理解:
语法:(目标类型)表达式或目标类型(表达式);
C++认为C风格的类型转换过于松散,可能会带来隐患,不够安全
C++推出了新的类型转换来替代C风格的类型转换,采用更严格的语法来检查,降低使用风险
C++新增了四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast,用于支持C++风格的类型转换
C++的类型转换只是语法上的解释,本质上与C风格的类型转换相同,C语言做不到的事情C++也做不到
语法:
static_cast<目标类型>(表达式);
const_cast<目标类型>(表达式);
reinterpret_cast<目标类型>(表达式);
dynamic_cast<目标类型>(表达式);
一、static_cast
1)用于内置数据类型之间的转换
除了语法不同,C和C++没有区别
示例:
int main()
{
int i = 3;
long y = i; //绝对安全,可以隐式转换,不会出现警告
double d = 1.23;
//long d1 = d; //可以隐式转换,但是,会出现可能丢失数据的警告
//long d2 = (long)d; //C风格:显式转换,不会出现警告
//long d3 = static_cast<long>(d); //C++风格:显式转换,不会出现警告
}
2)用于指针之间的转换
C风格可以把不同类型的指针进行转换
C++不可以,需要借助void*
示例:
void func(void*ptr){//其它类型指针->void*指针->其他类型指针
double* p = static_cast<double*>(ptr);
}
int main(int argc, char* argv[])
{
int i = 10;
double* pd1 = &i; //错误,不能隐式转换
double* pd2 = (double*)&i; //C风格,强制转换
double* pd3 = static_cast<double*>(&i); //错误,static_cast不支持不同类型指针的转换
void* pv = &i; //任何类型的指针都可以隐式转换成void*
double* pd4 = static_cast<double*>(pv); //static_cast可以把void*转换成其它类型的指针
func(&i);
}
二、const_cast
static_cast不能丢掉指针(引用)的const和volitale属性,const_cast可以
三、reinterpret_cast
static_cast不能用于转换不同类型的指针(引用)(不考虑有继承关系的情况),reinterpret_cast可以
reinterpret_cast的意思是重新解释,能够将一种对象类型转换为另一种,不管他们是否有关系
语法:reinterpret_cast<目标类型>(表达式);
<目标类型>和(表达式)中必须有一个是指针(引用)类型
reinerpret_cast不能丢掉(表达式)的const或volitale属性
应用场景:
-
reinterpret_cast的第一种用途是改变指针(引用)的类型
-
reinterpret_cast的第二种用途是将指针(引用)转换成整型。整型与指针占用的字节数必须一致,否则转换可能损失精度
-
reinterpret_cast的第三种用途是将一个整型转换成指针(引用)
7、 string容器
string是字符容器,内部维护了一个动态的字符数组
与普通的字符数组相比,string容器有三个优点:
- 使用的时候,不必考虑内存分配和释放的问题
- 动态管理内存(可扩展)
- 提供了大量操作容器的API
缺点:效率略有降低
string类是是std::basic_string类模板的一个具体化版本的别名
using std::string=std::basic_string<char,std::char_traits <char>,std::allocator <char>>
一、构造和析构
静态常量成员string::npos为字符数组的最大长度(通常为unsigned int的最大值)
NBTS(null-terminated string的简写) :C风格的字符串(以空字符0结束的字符串)
string类有七个构造函数(C++11新增了两个):
- string():创建一个长度为0的string对象(默认构造函数)
- string(const char *s):将string对象初始化为s指向的NBTS(转换函数)
- string(const string &str):将string对象初始化为str(拷贝构造函数——深拷贝)
- string(const char *s,size_t n):将string对象初始化为s指向的NBTS的前n个字符,即使超过了NBTS结尾
- string(const string &str,size_t pos=0,size_t n=npos):将string对象初始化为str从位置pos开始到结尾的字符,或从位置pos开始的n个字符
- template < class T>string(T begin,T end):将string对象初始化为区间[begin, end]内的字符,其中begin和end的行为就像指针,用于指定位置,范围包括begin在内,但不包括end
- string(size_t n,char c):创建一个由n个字符c组成的string对象
示例:
#include<iostream>
#include<string> //可不写
using namespace std;
int main()
{
//1、string():创建一个长度为0的string对象(默认构造函数)
string s1; //创建一个长度为0的string对象
cout << "s1=" << s1 << endl; //将输出s1=
cout<< "s1.capacity()=" << s1.capacity() << endl; //capacity()方法用于获取容器的当前容量,意思是如果不重新分配内存,当前可以存放字符的总数
cout<<"s1.size()=" << s1.size() << endl; //size()方法用于获取容器中数据的大小,意思是里面已经存放了多少数据
cout << "容器动态数组的首地址=" << (void*)s1.c_str() << endl;
s1 = "xxxxxxxxxxxxxxxxx";
cout << "s1.capacity()=" << s1.capacity() << endl; //返回当前容量,可以存放字符的总数
cout << "s1.size()=" << s1.size() << endl; //返回容器中数据的大小
cout << "容器动态数组的首地址=" << (void*)s1.c_str() << endl;
//2、string(const char* s) :将string对象初始化为s指向的NBTS(转换函数)
string s2("hello world");
cout << "s2=" << s2 << endl;
string s3 = "hello world";
cout << "s3=" << s3 << endl;
//3、string(const string & str):将string对象初始化为str(拷贝构造函数)
//注意:string类中有一个指向动态数组的指针,所以,string类的拷贝构造函数一定是深拷贝
string s4(s3);
cout << "s4=" << s4 << endl;
string s5 = s3;
cout << "s5=" << s5 << endl;
//4、string(const char* s, size_t n):将string对象初始化为s指向的NBTS的前n个字符,即使超过了NBTS结尾
string s6("hello world",5);
cout << "s6=" << s6 << endl;
string s7("hello world",50);
cout << "s7=" << s7 << endl; //输出s7=ello未知内容
//5、string(const string & str, size_t pos = 0, size_t n = npos) :将string对象初始化为str从位置pos开始到结尾的字符,或从位置pos开始的n个字符
//会判断str的结尾标志
string s8(s3,3, 5); //s3="hello world"
cout << "s8=" << s8 << endl; //将输出s8=lo wo
string s9(s3,3);
cout << "s9=" << s9 << endl; //将输出s9=lo world
cout << "s9.capacity()=" << s9.capacity() << endl; //返回当前容量,可以存放字符的总数
cout << "s9.size()=" << s9.size() << endl;
string s10("hello world",3, 5);
cout << "s10=" << s10 << endl; //将输出s10=lo wo
string s11("hello world", 3); //注意:不会用构造函数5,而是用构造函数4
cout << "s11=" << s11 << endl; //将输出s11=hel
//6、template < class T>string(T begin, T end):将string对象初始化为区间[begin, end]内的字符,
// 其中begin和end的行为就像指针,用于指定位置,范围包括begin在内,但不包括end
//7、string(size_t n, char c):创建一个由n个字符c组成的string对象
string s12(8, 'x');
cout << "s12=" << s12 << endl; //将输出s12=xxxxxxxxx
cout << "s12.capacity()=" << s12.capacity() << endl; //s12.capcacity()=15
cout << "s12.size()=" << s12.size() << endl; //s12.size()=8
string s13(30, 0);
cout << "s13=" << s13 << endl; //将输出s9=lo world
cout << "s13.capacity()=" << s13.capacity() << endl; //s13.capacity()=31
cout << "s13.size()=" << s13.size() << endl; //s12.size()=30
}
二、特性操作
int max_size() const; //返回string对象的最大长度string::npos,此函数意义不大
int capscity() const; //返回当前容量,可以存放字符的总数
int length() const; //返回容器中数据的大小(字符串语义)
int size() const; //返回容器中数据的大小(容器语义)
bool empty() const; //判断容器是否为空
void clear(); //清空容器
void reserve(size_t size=0); //将容器的容量设置为至少size
void resize(int len,char c=0); //把容器的实际大小设置为len,如果len<实际大小,会截断多出的部分;反之,就用字符c填充
三、字符操作
char &operator[] ( int n);
const char &operator[] ( int n)const; //只读
char &at( int n);
const char &st ( int n)const; //只读
operator[]和at()返回容器中的第n个元素,但at函数提供范围检查,当越界时会抛出out_of_range异常,operator[]不提供范围检查
const char*c_str()const; //返回容器中动态数组的首地址,语义:寻找以null结尾的字符串
const char*data()const; //返回容器中动态数组的首地址,语义:只关心容器中的数组
int copy(char *s,int n,int pos=0)const; //把当前容器中的内容,从pos开始的n个字节拷贝到以s中,返回实际拷贝的数目
四、赋值操作
1)string &operator=(const string &str); //把容器str赋值给当前容器
2)string &assign(const char*s); //将string对象赋值为s指向的NBTS
3)string &assign(const string &str); //将string对象赋值为str
4)string &assign(const char*s,size_t n); //将string对象赋值为s指向的地址后n字节的内容
5)string &assign(const string &str,size_t pos=0,size_t n=npos); //将string对象赋值为str从位置pos开始到结尾的字符(或从位置pos开始的n个字符)
6)template < class T>string &assign(T begin,T end); //将string对象赋值为区间[begin, end]内的字符
7)string &assign(size_t n,char c); //将string对象赋值为由n个字符c
五、连接操作
把内容追加到已存在容器的后面
1)string &operator+=(const string &str); //把容器str连接到当前容器
2)string &append(const char *s); //把指向s的NBTS连接到当前容器
3)string &append(const string &str); //把容器str连接到当前容器
4)string &append(const char *s,size_t n); //将s指向的地址后n字节的内容连接到当前容器
5)string &append(const string &str,size_t pos=0,size_t n=npos); //将str从位置pos开始到结尾的字符(或从位置pos开始的n个字符)连接到当前容器
6)template < class T>string &append(T beging,T end); //将区间[beng,end]内的字符连接到容器
7)string &append(size_t n,char c); //将n个字符c连接到当前容器
六、交换操作
void swap(string &str); //把当前容器与str交换
如果数据量很小,交换的是动态数组中的内容,反之,交换的是动态数组的地址
七、截取操作
string substr(size_t pos=0,size_t n=npos)const; //返回pos开始的n个字节组成的子容器
八、比较操作
bool operator==(const string &str1,const string &str2)const; //比较两个字符串是否相等
int compare(const string &str)const; //比较当前字符串和str1的大小
九、查找操作
size_t find(char c,size_t pos=0)const; //从pos开始查找字符c在当前字符串的位置
size_t find(const char*s, size_t pos=0)const; //从pos开始查找字符串s在当前串中的位置
size_t find(const char*s,size_t pos,size_t n)const; //从pos开始查找字符串s中前n个字符在当前串中的位置
size_t find(const string &str ,size_t pos=0)const; //从pos开始查找字符串s在当前串中的位置
查找成功时返回所在位置,失败返回string::npos
十、替换操作
string& replace(size_t pos,size_t len,const string& str);
string& replace(size_t pos,size_t len,const string& str,size_t subpos,size_t sublen=npos);
string& replace(size_t pos,size_t len,const char*s);
string& replace(size_t pos,size_t len,const char*s,size_t n);
string& replace(size_t pos,size_t len,size_t n,char c);
十一、插入操作
string& insert(size_t pos,const string& str);
string& insert(size_t pos,const string& str,size_t subpos,size_t sublen=npos);
string& insert(size_t pos,const char*s);
string& insert(size_t pos,const char*s,size_t n);
string& insert(size_t pos,size_t n,char c);
十二、删除操作
string &erase(size_t pos=0,size_t n=npos); //删除pos开始的n个字符
7.3string容器——设计目标
char cc[8]是什么?
- 可以存放7个字符的字符串
- 存放8个字符的字符数组
- 一块8字节的内存空间
string是什么?
-
size()==8的string对象是什么意思?
是已使用了8字节的内存空间
-
以字节为最小存储单元的动态容器
用于存放字符串(不存空字符0)
用于存放数据的内存空间(缓冲区)
string内部的三个指针
- char*start_; 动态分配内存块开始的地址
- char*end_; 动态分配内存块最后的地址
- char*finish_; 已使用空间的最后的地址
示例:
#define_CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int main()
{
char cc[8]; //在栈上分配8字节的内存空间
//把cc的内存空间用字节字符串
strcpy(cc, "hello");
cout << "cc=" << cc << endl<<endl;
////把cc的内存空间用于int型整数
//int* a, * b;
//a = (int*)cc; //前4个字节的空间用于整数a
//b = (int*)cc + 4; //后4个字节的空间用于整数b
//*a = 12345;
//*b = 54321;
//cout << "*a=" << *a << endl;
//cout << "*b=" << *b << endl << endl;
//把cc的内存空间用于double
double* d = (double*)cc;
*d = 12345.7;
cout << "*d=" << *d << endl << endl;
//把cc的内存空间用于结构体
struct stt
{
int a;
char b[4];
}*st;
st = (struct stt*)cc;
st->a = 38;
strcpy(st->b, "abc");
cout << "st->a=" << st->a << endl;
cout << "st->b=" << st->b << endl;
//void malloc(size_t size);
//char* cc1 = (char*)malloc(8);
}
示例:(有bug,内存泄漏)
#define_CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int main()
{
struct st_girl { //超女结构体
int bh;
char name[30];
bool yz;
double weight;
string memo;
}girl;
cout << "超女结构体的大小:" << sizeof(struct st_girl) << endl;
string buffer; //创建一个空的string容器buffer
//生成10名超女的信息,存入buffer中
for (int ii = 1; ii <<= 10; ii++)
{
//对超女结构体成员赋值
memset(&girl, 0, sizeof(struct st_girl));
girl.bh = ii;
sprintf(girl.name, "西施%02d", ii);
girl.yz = true;
girl.weight = 48.5 + ii;
girl.memo = "中国历史第一美女";
//把超女结构体追加到buffer中
buffer.append((char*)&girl, sizeof(struct st_girl));
}
cout << "buffer.capacity()=" << buffer.capacity() << endl; //显示容量
cout << "buffer.size()=" << buffer.size() << endl; //显示实际大小
//用一个循环,把buffer容器中全部的数据取出来
for (int ii = 0; ii << buffer.size() / sizeof(struct st_girl); ii++)
{
memset(&girl, 0, sizeof(struct st_girl)); //初始化超女结构体
//把容器中的数据复制到超女结构体
memcpy(&girl, buffer.data() + ii * sizeof(struct st_girl), sizeof(struct st_girl));
//buffer.copy((char*)&girl,sizeof(struct st_girl),ii*sizeof(struct st_girl));
//显示超女结构体成员的值
cout << "bh=" << girl.bh << ",name=" << girl.name << ",yz=" << girl.yz << ",weight=" << girl.weight << ",memo=" << girl.memo << endl;
}
}
8、vector容器
vector容器封装了动态数组。
包含头文件:#include < vector >
vector类模板的声明:
template<classT,class Alloc = allocator< T >>
class vector {
private:
T*start_;
T* finish_;
T* end_;
分配器
各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存<如果省略该模板参数的值,将默认使用allocator< T>,用new和delete分配和释放内存。
一、构造和析构
- 1)vector(); // 创建一个空的vector容器。
示例:
#include<iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>v; //创建一个空的vector容器。 cout<<"v.capacity()="<<v.capacity()<<", v.size()="<<v.size()<< endl;
}
- 2)explicit vector(const size_t n); //创建vector容器,元素个数为n(容量和实际大小都是n)。
示例:
int main()
{
// 1)创建一个空的vector容器。
vector<int>v1;
cout <<"v1.capacity0="<<v1.capacity() <<", v1.size0="<< v1.size() << endl;
// //2)创建vector容器,元素个数为n(容量和实际大小都是n)。
vector<int>v2(8);
cout<<"v2.capacity0="<<v2.capacity()<<",v2.size0="<<v2.size()<< endl;
}
- 3)vector(const vector< T>&v); // 拷贝构造函数。
- 4)vector(const size_t n,const T&value); // 创建vector容器,元素个数为n,值均为value。
- 5)vector(lterator first,Iterator last); //用迭代器创建vector容器。
- 6)vector(vector< T>&&v); //移动构造函数。
- 7)vector(initializer_list< T>il); //使用统一初始化列表。
析构函数~vector()释放内存空间
二、特性操作
size_tmax_size() const; //返回容器的最大长度,此函数意义不大。
size_t capacity() const; //返回容器的容量。
size_t size() const; //返回容器的实际大小(已使用的空间)
bool empty() const; //判断容器是否为空。
void clear(); //清空容器。
void reserve(size_t size); //将容器的容量设置为至少size。
void shrink_to_fit(); //将容器的容量降到实际大小(需要重新分配内存)从把容器的实际大小置为size
void resize(size_t size); //把容器的实际大小设置为size
voidresize(size_t size,const T &value); //把容器的实际大小置为size,如果size<实际大小,会截断多出的部分;如果size>实际大小,就用value填充。
三、元素操作
vector < int> v;
T &operator [] (size_t n);
constT &operator [] (size_ t n) const; //只读。
T &at(size_t n);
const T &at(size_t n) const; // 只读。
T * data(); //返回容器中动态数组的首地址。
const T*data() const; //返回容器中动态数组的首地址。
T &front(); //第一个元素
T &back(); //最后一个元素
四、赋值操作
给已存在的容器赋值,将覆盖容器中原有的内容。
- vectar &operator=(const vector &v); //把容器v赋值给当前容器。
- vector&operator=(initializer_list < T >il); //用统一初始化列表给当前容器赋值。
- void assign(initializer_list < T> il); //使用统一初始化列表赋值。
- void assign(Iterator first,Iterator last); //用迭代器赋值。
- void assign(const size_t n, const T& value); // 把 n 个 value 给容器赋值。
·五、交换操作
void swap(vector < T> &v); //把当前容器与v交换。
交换的是动态数组的地址。
六、比较操作
bool operator==(const vector< T>&v)const;
bool operator!=(const vector< T> &v) const;
七、插入和删除
1)void push_back(const T& value); // 在容器的尾部追加一个元素。
2)void emplace_back(...); //在容器的尾部追加一个元素,..用于构造元素。C++11
3)iterator insert(iterator pos,const T& value); //在指定位置插入一个元素,返回指向插入元素的迭代器。
4)iterator emplace(iterator pos,..); //在指定位置插入一个元素,...用于构造元素,返回指向插入元素的迭代器。C++11
5)iterator insert(iterator pos,iterator first, iterator last); // 在指定位置插入一个区间的元素,返回指向第一个插入元素的迭代器
6)void pop_back(); //从容器尾部删除一个元素。
7)iterator erase(iterator pos); //删除指定位置的元素,返回下一个有效的迭代器。
8)iterator erase(iterator first,iterator last); //删除指定区间的元素,返回下一个有效的迭代器
vector容器的本质是数组,数组的内存空间是连续的,如果在中间插入元素,再把后面的元素全部往后移动,如果删除中间的某个元素,要把后面的元素全部往前移动,移动元素的效率是很低的,所以,尽可能不要再vector容器的中间插入和删除元素
八、vector的嵌套
vector容器可以嵌套使用。
示例:
#include<iostream>
#include <vector>
using namespace std;
int main()
{
vector<vector<int>>vv; //创建一个vector容器vw,元素的数据类型是vector<int>。
vector<int>v; //创建一个容器v,它将作为容器wv的元素。
v = { 1,2,3,4,5 }; //用统一初始化列表给v赋值。
vv.push_back(v); //把容器v作为元素追加到vv中。
v = { 11,12,13,14,15,16,17 }; //用统一初始化列表给v赋值。
vv.push_back(v); //把容器v作为元素追加到vv中。
v = { 21,22,23 }; //用统一初始化列表给v赋值。
vv.push_back(v); //把容器v作为元素追加到vv中。
//用嵌套的循环,把w容器中的数据显示出来。
for (int ii = 0; ii < vv.size(); ii++)
{
for (int jj = 0; jj < vv[ii].size(); jj++)
cout << vv[ii][jj] << " "; //像二维数组一样使用容器。
cout << endl;
}
}
九、注意事项
1)迭代器失效的问题
resize()、reserve()、assign()、push_back()、pop_back()、insert()、erase()等函数会引起 vector容器的动态数组发生变化,可能导致vector迭代器失效。
9、迭代器
迭代器是访问容器中元素的通用方法
如果使用迭代器,不同的容器,访问元素的方法是相同的
迭代器支持的基本操作:赋值(=)、解引用(*)、比较(==和!=)、从左向右遍历(++)
一般情况下,迭代器是指针和移动指针的方法
示例:
#include<iostream>
#include<vector>
#include<list>
using namespace std;
struct Node //单链表的结点
{
int item;
Node* next;
};
int* find_(int* begin, int* end, const int& val) //在整型数组的区间中查找值为val的元素
{
for (int*iter = begin; iter!=end; iter++) //遍历查找区间
if (*iter== val)return iter; //如果找到了,返回区间中元素的地址
return nullptr;
}
Node* find_(Node* begin, Node* end, const Node& val) //在单链表中查找值为var的元素
{
for (Node* iter = begin; iter != end; iter = iter->next) //遍历链表
if (iter->item == val.item)return iter;
return nullptr;
}
//查找元素的算法
template<typename T1,typename T2>
//begin-查找区间开始的位置;end-查找区间结束的位置;val-待查找的值
T1 find_(T1 begin, T1 end, const T2& val)
{
for (T1 iter = begin; iter != end; iter++) //遍历查找区间
if (*iter == val)return iter; //如果找到了元素,返回区间中的位置
return end;
}
int main()
{
//在vector容器中查找元素
vector<int>vv = { 1,2,3,4,5 };
vector<int>::iterator it2 = find_(vv.begin(), vv.end(), 3);
if (it2 != vv.end())cout << "查找成功。\n";
else cout << "查找失败。\n";
//在list容器中查找元素
list<int>ll = { 1,2,3,4,5 };
list<int>::iterator it3 = find_(ll.begin(), ll.end(), 3);
if (it3 != ll.end())cout << "查找成功。\n";
else cout << "查找失败。\n";
}
迭代器有五种分类:
1)正向迭代器
只能使用++运算符来遍历容器,每次沿容器向右移动一个元素
容器名 <元素类型>::iterator 迭代器名; //正向迭代器
容器名 <元素类型>::const_iterator 迭代器名; //常正向迭代器
相关函数:
iterator begin();
const_iterator chegin(); //配合auto使用
iterator end();
const_iterator end();
const_iterator cend();
示例:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>vv = { 1,2,3,4,5 }; //初始化vector容器
vector<int>::iterator it1 = vv.begin(); //容器的开始
*it1 = 8; //将修改容器中第0元素
it1++; //迭代器后移一个元素
*it1 = 7; //将修改容器中第1元素
for (vector<int>::const_iterator it2 = vv.cbegin(); it2 != vv.end(); it2++)
cout << *it2 << " ";
}
2)双向迭代器
具备正向迭代器的功能,还可以反向(从右到左)遍历容器(也是用++),不管是正向还是反向遍历,都可以用--让迭代器后退一个元素。
容器名<元素类型>::reverse_iterator迭代器名;//反向迭代器。
容器名<元素类型>::const_reverse_iterator迭代器名;//常反向迭代器。
相关的成员函数:
reverse_iterator rbegin();
const _reverse_iterator crbegin();
reverse_iterator rend();
const_reverse_iterator crend();
3)随机访问选代器
具备双向迭代器的功能,还支持以下操作:
- 用于比较两个迭代器相对位置的关系运算(<、<=、>、>=)
- 迭代器和一个整数值的加减法运算(+、+=、-、-=)。
- 支持下标运算(iter[n])。
数组的指针是纯天然的随机访问选代器。
4)输入和输出迭代器
这两种迭代器比较特殊,它们不是把容器当做操作对象,而是把输入/输出流作为操作对象。
10、基于范围的for循环
示例:
#include<iostream>
#include <vector>
using namespace std;
int main()
{
vector<int >vv = { 1,2,3,4,5,6,7,8,9,10 };
for (auto it = vv.begin(); it != vv.end();it++) // 用迭代器遍历容器vv。 it++)
{
cout << *it << " ";
}
cout << endl;
//用基于范围的for循环遍历容器vw。
for (int val : vv)
{
cout << val << " ";
}
cout << endl;
}
对于一个有范围的集合来说,在程序代码中指定循环的范围有时候是多余的,还可能犯错误。
C++11 中引l入了基于范围的for循环。
语法:
for(迭代的变量:迭代的范围)
{
//循环体。
}
注意:
- 迭代的范围可以是数组名、容器名、初始化列表或者可迭代的对象(支持begin()、end()、++、==)。
- 数组名传入函数后,已退化成指针,不能作为容器名。
- 如果容器中的元素是结构体和类,迭代器变量应该申明为引l用,加const约束表示只读。
示例:
#include<iostream>
#include <vector>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << "默认构造函数AA().\n"; }
AA(const string& name) :m_name(name) { cout << "构造函数,name=" << m_name << ".\n"; }
AA& operator=(const AA& a) {
m_name = a.m_name; cout << "赋值函数,name=" << m_name << ".\n"; }
~AA() { cout << "析构函数,name=" << m_name << ".\n"; }
};
int main()
{
vector<AA>v;
cout << "1,v.capacity()=" << v.capacity() << endl;
v.emplace_back("西施");
cout << "2,v.capacity()=" << v.capacity() << endl;
v.emplace_back("冰冰");
cout << "3,v.capacity()=" << v.capacity() << endl;
v.emplace_back("咪咪");
cout << "4,v.capacity()=" << v.capacity() << endl;
for (auto a : v)
cout << a.m_name << "";
cout << endl;
}
- 注意迭代器失效的问题。
11、list容器
list容器封装了双链表。
包含头文件:#include < list >
list类模板的声明:
template<class T,class Alloc=allocator<T>>
class list{
private:
iterator head;
iterator tail;
....
}
一、构造函数
- list(); // 创建一个空的list容器。
- list(initializer_list< T>il); //使用统一初始化列表。
- list(const list< T>&v); // 拷贝构造函数。
- list(Iterator first,Iterator last); //用迭代器创建list容器。
- list(list< T>&&v); // 移动构造函数(C++11标准)。
- explicit list(const size_t n); //创建list容器,元素个数为n。
- list(const size_t n, const T& value); //创建list容器,元素个数为n,值均为value。析构函数~list(释放内存空间。
示例:
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
// 1)创建一个空的list容器。
list<int> L1;
// cout<<"Li.capacityO="<<L1.capacity0<<endl; //链表没有容量说法。
cout<<"li.size()="<<L1.size()<< endl;
//2)list(initalizer_list<T>il) 使用统一初始化列表创建list容器。
list<int> L2({ 1, 2, 3, 4, 5, 6,7, 8, 9, 10 }); //1、把统一初始化列表作为实参,写在()中
//list<int>12={1,2,3,4,5,6,7,8,9,10}; //2、把这个构造函数用于转换函数
//list<int>12{1,2,3,4,5,6,7,8,9,10}; //3、省略第二种写法的“=”
for (int value : L2) //用基于范围的for循环遍历容器。
cout<<value<<" ";
cout<< endl;
//3)list(const list< T>&v); // 拷贝构造函数。
list<int>L3(L2);
//list<int>L3 = L2;
for (int value : L3)
cout << value << " ";
cout << endl;
//4)list(Iterator first, Iterator last); //用迭代器创建list容器。
list<int>L4(L3.begin(), L3.end());
for (int value : L4)
cout << value << " ";
cout << endl;
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 创建vector容器。
list<int> L5(v1.begin() + 2, v1.end() - 3); //用vector容器的选代器创建list容器。
for (int value : L5)
cout << value << " ";
cout << endl;
int a1[]={1,2,3,4,5,6,7,8,9,10}; //创建数组。
list<int>L6(a1 + 2, a1 + 10 -3); //用数组的指针作为选代器创建list容器
for (int value : L6)
cout << value << " ";
cout << endl;
char str[] = "hello world"; //定义C风格字符串。
string s1(str + 1, str + 7); //用C风格字符串创建string容器。
for (auto value : s1) //遍历string容器。
cout << value << " ";
cout << endl;
cout << s1 << endl; //以字符串的方式显示string容器。
vector<int>v2(L3.begin(),L3.end()); //用list迭代器创建vector容器。
for (auto value : v2) //遍历vector容器。
cout << value << " ";
cout << endl;
}
二、特性操作
- size_t max size() const; //返回容器的最大长度,此函数意义不大。
- size_t capacity() const; //返回容器的容量。
- size_t size() const; //返回容器的实际大小(已使用的空间)
- bool empty() const; //判断容器是否为空。
- void clear(); //清空容器。
- void reserve(size_t size); //将容器的容量设置为至少size。
- void shrink_to_fit(); //将容器的容量降到实际大小(需要重新分配内存)。
- void resize(sizent size); //把容器的实际大小置为size。
三、元素操作
- T &front(); //第一个元素。
- const T &front(); //第一个元素,只读。
- const T &back(); //最后一个元素,只读。
- T &back(); //最后一个元素。
四、赋值操作
给已存在的容器赋值,将覆盖容器中原有的内容。
- list &operator=(const list &L); //把容器L赋值给当前容器。
- list &operator=(initializer_list < T >il); //用统一初始化列表给当前容器赋值。
- list assign(initializer_list < T> il); //使用统一初始化列表赋值。
- list assign(Iterator first,Iterator last); //用迭代器赋值。
- list assign(const size_t n, const T& value); // 把 n 个 value 给容器赋值。
五、交换、反转、排序、归并
void swap(list
void reverse(); //反转链表。
void sort(); //对容器中的元素进行升序排序。
void sort( _ Pr2 _ Pred); //对容器中的元素进行排序,排序的方法由_Pred决定(二元函数)
void merge(list
示例:
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
list<int> la = { 8,2,4,6,7 };
for (auto& val : la)
cout << val << " ";
cout << endl;
la.reverse(); //反转链表
for (auto& val : la)
cout << val << " ";
cout << endl;
la.sort(); //链表排序
for (auto& val : la)
cout << val << " ";
cout << endl;
list<int> lb = { 3,7,9,10,1 };
lb.sort(); //链表排序
la.merge(lb); //归并链表
for (auto& val : la)
cout << val << " ";
cout << endl;
}
六、比较操作
bool operator ==(constvector<T>&l) const;
bool operator!=(const vector<T>&l) const;
七、插入和删除
1.voidpush_back(const T&value); //在容器的尾部追加一个元素。
- void emplace_back(...); //在容器的尾部追加一个元素,..用于构造元素。C++11
- iterator insert(iterator pos,const T& value); //在指定位置插入一个元素,返回指向插入元素的迭代器。
- iterator emplace(iterator pos,..); //在指定位置插入一个元素,..用于构造元素,返回指向插入元素的迭代器。C++11
- iterator insert(iterator pos,iterator first,iterator last); //在指定位置插入一个区间的元素,返回指向第一个插入元素的迭代器。
- void pop_back(); //从链表尾部删除一个元素。
- iterator erase(iterator pos); //删除指定位置的元素,返回下一个有效的迭代器。
- iterator erase(iterator first,iterator last); //删除指定区间的元素,返回下一个有效的迭代
- push_front(const T&value); //在链表的头部插入一个元素。
- emplace_front(...); //在链表的头部插入一个元素,..用于构造元素。C++11
- splice(iterator pos, const vector< T> & I); //把另一个链表连接到当前链表。
- splice(iterator pos,const vector< T>&l, iterator first, iterator last); // 把另一个链表指定的区间连接到当前链表。
- splice(iterator pos, const vector< T> & 1, iterator first); //把另一个链表从first开始的结点连接到当前链表。
- void remove(value); //删除链表中所有等于value的元素。
- void remove_if( _ Pr1 _ Pred); //删除链表中满足条件的元素,参数 _Pred是一元函数。
- void unique(); //删除链表中相邻的重复元素,只保留一个
示例:
int main()
{
list < int>La = { 8,2,2,3,6,3,3,3,3,3,2,5 };
for (auto& val : La) cout << val << " ";
cout << endl;
La.unique(); //删除链表中相邻的重复元素,只保留一个。
for (auto& val : La)
cout << val <<" ";
cout << endl;
}
12、pair键值对
pair是类模板,一般用于表示key/value 数据,其实现是结构体。
pair结构模板的定义如下:
template <class T1, class T2>
struct pair
{
T1 first; //第一个成员,一般表示key。
T2 second; //第二个成员,一般表示value。
pair(); //默认构造函数。
pair(const T1 &val1,const T2 &val2); //有两个参数的构造函数。
pair(const pair<T1,T2> &p); //拷贝构造函数
void swap(pair<T1,T2> &p); //交换两个pair
};
makepair函数模板的定义如下:
template<class T1,class T2
make_pair(constT1&first,constT2&second)
{
return pair<T1,T2>(first, second);
}
13、红黑树(平衡二叉排序树)
左边的结点一定要比自己小,右边的结点一定要比自己大(左小右大)
红黑树查找元素的方法也叫二分查找
在实际开发中,如果数据量很小,用数组加二分查找的效率更高,如果数据量很大,用红黑树更合适,链式存储在内存管理上更方便
14、map容器
map 容器封装了红黑树(平衡二叉排序树),用于查找。〈
包含头文件:#include
map 容器的元素是 pair 键值对。
map类模板的声明:
template <class K, class V, class P = less< K>, class _ Alloc = allocator<pair<const K, V >>>class map : public _ Tree<_Tmap_traits< K, V, P, _Alloc, false>>
{
...
}
第一个模板参数K:key的数据类型(pair.first)
第二个模板参数V:value的数据类型(pair.second) 。
第三个模板参数P:排序方法,缺省按key升序。
第四个模板参数_Alloc:分配器,缺省用new和delete。
map提供了双向迭代器
二叉链表:
struct BTNode
{
pair<K,V>p; //键值对。
BTNode *parent; //父节点。
BTNodee *lchirld; //左子树
BTNode *rchild; //右子树。
};
一、构造函数
1)map(); // 创建一个空的map容器。
2)map(initializer_list<pair<K,V>>il); //使用统一初始化列表。
3)map(const map<K,V>&m); // 拷贝构造函数。
4)map(lterator first, Iterator last);; //用迭代器创建map容器。
5)map(map<K,V>&& m);; //移动构造函数(C++11标准)
二、特性操作
size_t size() const; //返回容器的实际大小(已使用的空间)
bool empty() const; //判断容器是否为空。
void clear(); // 清空容器。
三、元素操作
V&operator [ ] (K key); //用给定的key访问元素。
const V&operator [ ] (K key)const; // 用给定的key访问元素,只读。
V &at(K key); //用给定的key访问元素。
const V &at(K key) const; //用给定的key访问元素,只读。
示例:
#include <iostream>
#include <map>
using namespace std;
int main()
{
map<string, string> m({ {"08","冰冰"},{"03","西施"},{"01","咪咪"},{"07","金莲"},{"05","丝瓜"} });
cout<< "m[08] = "<< m["08"]<< endl; //显示key为08的元素的value。
cout<<"m[09] = "<< m["09"] << endl; //显示key为09的元素的value。key为09的元素不存在,将添加新
m["07"]="花花"; //把key为07的元素的value修改为花花。
m["12"]="小乔"; // 将添加新的键值对。
for (auto& val : m)
cout << val.first << "," << val.second << " ";
cout << endl;
}
注意:
1)[ ] 门运算符:如果指定键不存在,会向容器中添加新的键值对;如果指定键不存在,则读取或修改容器中指定键的值。
2)at()成员函数:如果指定键不存在,不会向容器中添加新的键值对,而是直接抛出out_of_range异常
四、赋值操作
给已存在的容器赋值,将覆盖容器中原有的内容。
map<K,V> &operator=(const map<K,V>& m); //把容器l赋值给当前容器。
map<K,V> &operator=(initializer_list<pair<K,V>>il); // 用统一初始化列表给当前容器赋值。
五、交换操作
void swap(map<K,V>& m); //把当前容器与m交换。
交换的是树的根结点。权对自勺辰结品个
六、比较操作
bool operator == (const map<K,V>& m) const;
bool operator != (const map<k,V>& m) const;
七、查找操作
1)查找键值为key的键值对
在map容器中查找键值为key的键值对,如果成功找到,则返回指向该键值对的迭代器;失败返回end().
iterator find(const K &key);
const_iterator find(const K &key) const; // 只读。
2)查找键值>=key 的键值对
在 map 容器中查找第一个键值>=key的键值对,成功返回迭代器;失败返回end()。
iterator lower_bound(const K &key);
const_iterator lower_bound(const K &key)const; // 只读。
3)查找键>key的键值对
在 map 容器中查找第一个键值>key的键值对,成功返回迭代器;失败返回end()。
iterator upper_bound(const K &key);
const _iterator upper_bound(const K &key) const; // 只读。
4)统计键值对的个数
统计 map 容器中键值为key的键值对的个数。
size_t count(const K &key) const;
八、插入和删除
1)void insert(initializer_list<pair<k,V>>il); //在容器中插入一个元素。
2)pair<iterator,bool>insert(constpair<k,V>&value); // 在容器中插入一个元素,返回值pair:first是已插入元素的迭代器,second是插入结果。
3)void insert(iteratorfirst,iteratorlast); //用迭代器插入一个区间的元素。
4)pair<iterator,bool>emplace(Args&&...args); //将创建新键值对所需的数据作为参数直接传入,map容器将直接构造元素。返回值pair:first是已插入元素的迭代器,second是插入结果。
5)iterator emplace_hint(const_iterator pos, Args&&..args); // 功能与第4)个函数相同个参数提示插入位置。该参数只有考意义,如果提示的位置是正确的,对性能有提升,如果提示的位
示例:
#include <iostream>
#include <map>
using namespace std;
class CGirl //超女类。
{
public:
string m_name; // 超女姓名。
int m_age;//超女年龄。
/*CGirl0 : m_age(0) {
cout<<“默认构造函数。\n";
}*/
CGirl(const string name, const int age) : m_name(name), m_age(age) {
cout << "两个参数的构造函数。\n";
}
CGirl(const CGirl& g) : m_name(g.m_name), m_age(g.m_age){
cout << "拷贝构造函数。\n";
}
};
int main()
{
map<int, CGirl> mm;
//mm.insert(pair<int, CGirl>(8, CGirl("冰冰", 18))); //一次构造函数,两次拷贝构造函数。
//mm.insert(make_pair<int,CGirl>(8,CGirl("冰冰",18)); // 以上面的一样
//mm.emplace(pair<int, CGirl>(8, CGirl("冰冰", 18))); //一次构造函数,两次拷贝构造函数。
//mm.emplace(make_pair<int, CGirl >(8, CGirl("冰冰", 18))); //以上面的一样
//mm.emplace(8,CGirl("冰冰",18)); //一次构造函数,一次拷贝构造函数。
//mm.emplace(8,"冰冰", 18); //错误。
mm.emplace(piecewise_construct,forward_as_tuple(8),forward_as_tuple("冰冰",18)); //一次构造函数
//上面()中的---第一个参数表示分段构造;第二个参数表示用8构造key;第三个参数表示用冰冰和18构造value,也就是超女类
for (const auto& val : mm)
cout << val.first << ",," << val.second.m_name << "," << val.second.m_name << " ";
cout << endl;
return 0;
map<int, string> m;
//1)void insert(initializer_list<pair<K,V>>il);//用统一初始化列表在容器中插入多个元素。
m.insert({ {8,"冰冰 "},{3,"西施 "} });
m.insert({ pair<int,string>(1,"蜜蜜 "),make_pair<int,string>(7,"金莲 "),{5,"西瓜 " } });
m.insert({ {18,"冰冰 "},{3,"西施 "} });
//2) pair<iterator,bool> insert(const pair<k,V> &value);//在容器中插入一个元素,返回值pair:first是已插入元素的迭代器,second是插入结果。
auto ret =m.insert(pair<int, string>(12,"花花 "));
if (ret.second == true) cout <<"插入成功:"<<ret.first->first<<","<< ret.first->second<< endl;
else cout<<"插入失败。\n";
//4)pair<iterator, bool>emplace(Args&&...args);
//将创建新键值对所需的数据作为参数直接传入,map容器将直接构造元素。
// 返回值pair:first是已插入元素的迭代器,second是插入结果。
auto ret1 = m.emplace(12, "花花 ");
if (ret1.second == true) cout << "插入成功:" << ret1.first->first << "," << ret1.first->second << endl;
else cout << "插入失败。\n";
for (auto& val : m)
cout << val.first << "," << val.second << "";
cout << endl;
}
15、哈希表
- 哈希表长(桶的个数):数组的长度。
- 哈希函数:size_t hash(const T &key)
- 装填因子:元素总数/表长,其值越大,效率越低。
- 常见方法:用key%小于表长的最大质数取余数。质数很特别,对它取余数可以把数据打得很散
16、unordered_map容器
unordered_map 容器封装了哈希表,查 找、插入和删除元素时,只需要比较几次key的值。
包含头文件: #include<unordered_map>
unordered_map容器的元素是pair键值对。
unordered_map类模板的声明:
template <class K, class V, class _ Hasher = hash< K>, class _Keyeq = equal _ to< K>,class _Alloc = allocator<pair<const K, V>>>_
class unordered_map : public _Hash<_Umap_traits<K, V, _ Uhash_compare<K, _HasherKeyeq>, _Alloc, false>>
{
......
}
第一个模板参数K:key 的数据类型(pair.first)。
第二个模板参数V:value的数据类型(pair.second)
第三个模板参数_Hasher:哈希函数,默认值为std:hash< K>
第四个模板参数_Keyeq:比较函数,用于判断两个key是否相等,默认值是std:equal_to< K>。
第五个模板参数Alloc:分配器,缺省用new和delete。
创建std:unordered_map类模板的别名:
template<classK,classV>using umap =std::unordered_map<K,V>
一、构造函数
1)umap(); // 创建一个空的umap容器。
2)umap(size_t bucket); // 创建一个空的umap容器,指定了桶的个数,下同。
3)umap(initializer_list<pair<K,V>>il); // 使用统一初始化列表。
4)umap(initializer_list<pair<K,V>>il, size_t bucket); // 使用统一初始化列表。
5)umap(Iterator first,Iterator last); //用迭代器创建umap容器。
6)umap(lterator first,Iterator last,size_tbucket); //用迭代器创建umap容器。
7)umap(const umap<K,V>&m); // 拷贝构造函数。
8)umap(umap<K,V>&& m); //移动构造函数(C++11标准)
二、特性操作
size_t size() const; //返回容器中元素的个数。
bool empty() const; //判断容器是否为空。
void clear()); //清空容器。
size_t max_bucket_count(); //返回容器底层最多可以使用多少桶,无意义。
size_t bucket_count(); //返回容器桶的数量。空容器有8个桶
示例:
#include<iostream>
#include <unordered_map>
using namespace std;
template<class K, class V>
using umap = std::unordered_map<K, V>;
int main()
{
umap<int, string> m;
cout << m.bucket_count() << endl; //空容器有8个桶。
size_t itmp = m.bucket_count();
for (int ii = 0; ii < 200000; ii++) //插入容器中插入若干记录。
{
char name[50]; sprintf_s(name, "西施%d", ii);
m.emplace(ii, name);
if (itmp != m.bucket_count()) {//如果桶自动扩展,显示日志。
cout << m.bucket_count() << endl;
itmp = m.bucket_count();
}
}
//for (auto& val : m) //遍历整个容器
// cout << val.first << "," << val.second << " ";
//cout << endl;
}
size_t bucket_size(size_t n); //返回第n个桶中元素的个数,0<=n<bucket_count()
size_t bucket(K &key); //返回值为key的元素对应的桶的编号。
float load-factor(); //返回容器的装填因子,load_factor()=size()/bucket_count()
示例:
int main()
{
umap<int, string> m;
cout << "最大装填因子:"<<m.max_load_factor()<<endl;
m.insert({{1,"西施1"},{2,"西施2"},{3,"西施3"},{4,"西施4"} });
cout<<"当前桶数:"<<m.bucket_count()<<endl;
cout<<"当前装填因子:"<<m.load_factor()<<endl;
m.insert({{5,"西施5"},{6,"西施6"},{7,"西施7"},{8,"西施8"} });
cout<<"当前桶数:"<<m.bucket_count()<<endl;
cout<<"当前装填因子:" << m.load_factor() << endl;
m.emplace(9, "西瓜");
cout << "当前桶数:" << m.bucket_count() << endl;
cout << "当前装填因子:"<<m.load_factor()<<endl;
}
示例:
int main()
{
umap<int, string> m;
m.max_load_factor(5); //设置最大装填因子为5,不要让容器扩展。
m.insert({ {1,"西施1"},{2,"西施2"}, {4,"西施4"}});
m.insert({{5,"西施5"},{6,"西施6"},{7,"西施7"},{8,"西施8"}});
m.insert({{15,"西施5"},{16,"西施6"},{17,"西施7"},{18,"西施8"}});
m.insert({{25,"西施5"},{26,"西施6"},{27,"西施7"},{28,"西施8"}});
m.emplace(9,"西瓜");
//for (auto& val : m) //遍历整个容器
// cout << val.first << "," << val.second << " ";
//cout << endl;
for (auto it = m.begin(); it != m.end(); it++)//遍历整个容器。
cout << it->first << "," << it->second << " ";
cout << endl;
for(int ii = 0; ii < m.bucket_count(); ii++) // 遍历全部的桶。
{
cout << "桶" << ii << ":";
for (auto it = m.begin(ii); it != m.end(ii); it++) // 遍历桶中的元素。
cout << it->first << "," << it->second << " ";
cout << endl;
}
}
三、元素操作
V &operator [ ] (K key); //用给定的key访问元素。
const V &operator [ ] (Kkey)const; // 用给定的key访问元素,只读。
V &at(K key); //用给定的key访问元素。
const V &at(K key) const; //用给定的key访问元素,只读。
注意:
1)[ ]运算符:如果指定键不存在,会向容器中添加新的键值对;如果指定键不存在,则读取或修改容器中指定键的值。
2)at()成员函数:如果指 容器中添加新的键值对,而是直接抛出out of range
四、赋值操作
给已存在的容器赋值,将覆盖容器中原有的内容。
1)umap<K,V> &operator=(const umap<K,V>& m); //把容器m赋值给当前容器。
2)umap<K,V>&operator=(initializer_list<pair<K,V>>il); // 用统一初始化列表给容器赋值。
五、交换操作
void swap(umap<K,V>& m); //把当前容器与m交换。
交换的是树的根结点。
六、比较操作
bool operator == (const umap<K,V>& m) const;
bool operator !=(const umap<K,V>& m) const;
七、查找操作
1)查找键值为key的键值对
在 map 容器中查找键值为key的键值对,如果成功找到,则返回指向该键值对的迭代器;失败返回end()。
iterator find(const K &key);
const_iterator find(const K &key)const; //只读
2)统计键值对的个数
统计umap容器中键值为key的键值对的个数
size_t count(const K &key)const;
八、插入和删除
1)voidinsert(initializer_list<pair<K,V>>il); //用统一初始化列表在容器中插入多个元素。
2)pair<iterator,bool>insert(constpair<K,V>&value); // 在容器中插入一个元素,返回值pair:first是已插入元素的选代器,second是插入结果。
3)voidinsert(iteratorfirst,iteratorlast); //用迭代器插入一个区间的元素。
4)pair<iterator,bool>emplace(..); //将创建新键值对所需的数据作为参数直接传入,map容器将直接构造元素。返回值pair:first是已插入元素的迭代器,second是插入结果。
例:mm.emplace(piecewise_construct, forward_as_tuple(8),forward_as_tuple("冰冰",18));
5)ierator emplace_hint(const_iterator pos,...); //功能与第4)的函数相同,第一个参数提示插入位置,该参数只有参考意义
6)size_terase(const K &key); //从容器中删除指定key的元素,返回已删除元素的个数。
7)iterator erase(iterator pos; //用迭代器删除元素,返回下一个有效的迭代器。
8)iterator erase(iterator first,iterator last); //用迭代器删除一个区间的元素,返回下一个有效的迭代器。
16、queue容器
特点:先进后出
queue容器的逻辑结构是队列,物理结构可以是数组或链表,主要用于多线程之间的数据共享。
**包含头文件: ** #include< queue>
queue 类模板的声明:
template <class T, class _Container = deque< T>>
class queue{
...
}
第一个模板参数T:元素的数据类型。
第二个模板参数_Container:底层容器,缺省是std::deque
queue容器不支持迭代器
一、构造函数
1)queue(); //创建一个空的队列。
2)queue(const queue
3)queue(queue< T>&&q); // 移动构造函数(C++11标准)。
析构函数~gueue()释放内存空间。
二、常用操作
1)voidpush(const T&value); // 元素入队。
2)void emplacel...); //元素入队,...用于构造元素。C++11
3)size_t size0 const; //返回队列中元素的个数。
4)bool empty0 const; //判断队列是否为空。
5)T &front(); //返回队头元素。
6)const T &front(); //返回队头元素,只读。
7)T &back(); //返回队尾元素。
8)const T &back(); //返回队头元素,只读。
9)void pop(); //出队,删除队头的元素。
示例:
#include<iostream>
#include <list>
using namespace std;
class girl //超女类
{
public:
int m_bh; //编号
string m_name; //姓名
girl(const int&bh,const string& name):m_bh(bh),m_name(name){}
};
int main()
{
// template <class T, class _Container = deque<T>>
// class queue{....}
//第一个模板参数T:元素的数据类型。
//第二个模板参数Container:底层容器的类型,缺省是std:deque,可以用std:list,还可以用自定义的类模板。
//queue<girl,list<girl>> q;//物理结构为链表。
//queue<girl,deque<girl>> q; //物理结构为数组。
//queue<girl>q; //物理结构为数组。
//queue<girl,vector<girl>>q; // 物理结构为数组,不可以。
q.push(girl(3, "西施")); //效率不高
q.emplace(8, "冰冰"); //效率不高
q.push(girl(5, "幂幂"));
q.push(girl(2, "西瓜"));
while (q.empty() == false)
{
cout << "编号:" << q.front().m_bh << ",姓名:" << q.front().m_name << endl;
q.pop();
}
}
三、其它操作
1)queue &operator=(const queue< T> &q); //赋值。
2)void swap(queue< T> &q); //交换.
3)booloperator(constqueue< T>&q)const; //重载操作符。
4)bool operator !=(const queue< T>&q)const; // 重载!=操作符。
17、STL其它容器
17.1 array(静态数组)
1)物理结构
在栈上分配内存,创建数组的时候,数组长度必须是常量,创建后的数组大小不可变。
template<class T, size_t size>
class array {
private:
Telems_[size];
...
};
2)迭代器
随机访问迭代器
3)特点
部分场景中,比常规数组更方便(能用于模板),可以取代常规数组
4)各种操作
1)void fill(const T & val); //给数组填充值(清零)
2)size_t size; //返回数组的大小。
3)bool empty() const; //无意义。
4)T&operator[ ] (size_t n);
5) const T &operator[ ] (size_t n) const; //只读。
6)T &at(size_t n);
7)const T &at(size t n) const;// 只读。
8)T * data(); //返回数组的首地址
9)constT*data()const; //返回数组的首地址。
10)T&front(); //第一个元素。
11)const T &front(); //第一个元素,只读。
12)const T &back()); //最后一个元素,只读。
13)T &back(); //最后一个元素。
17.2 deque(双端队列)
1)物理结构
deque容器存储数据的空间是多段等长的连续空间构成,各段空间之间并不一定是连续的。为了管理这些连续空间的分段,deque容器用一个数组存放着各分段的首地址
通过建立数组,deque容器的分段的连续空间能实现整体连续的效果
当deque容器在头部或尾部增加元素时,会申请一段新的连续空间,同时在数组中添加指向该空间的指针
2)迭代器
随机访问迭代器
3)特点
- 提高了在两端插入和删除元素的效率。
- 在中间插入和删除元素的效率比vector更糟糕
- 随机访问的效率比vector容器略低。
4)各种操作
与vector容器相同
17.3 forward list(单链表)
1)物理结构
单链表
2)迭代器
正向迭代器
3)特点
比双链表少了一个指针,可节省一丢丢内存,减少了两次对指针的赋值操作
如果单链表能满足业务需求,建议使用单链表而不是双链表
4)各种操作
与list容器相同
17.4 multimap
底层是红黑树。
multimap和 map的区别在:multimap允许关键字重复,而map不允许重复。
各种操作与 map 容器相同。
17.5 set & multiset
底层是红黑树。
set和map的区别在:map 中存储的是键值对,而set只保存关键字。
multiset和set的区别在:multiset允许关键字重复,而 set不允许重复。
各种操作与map容器相同
17.6 unordered_multimap
底层是哈希表。
unordered_multimap和unordered_map的区别在:unordered_multimap允许关键字重复,而unordered_map不允许重复。
各种操作与unordered_map容器相同。
17.7 unordered_set&unordered_multiset
底层是哈希表。
unordered_set和unordered_map的区别在:unordered_map中存储的是键值对,而unordered_set只保存关键字。
unordered_multiset和unordered_set的区别在:unordered_multiset允许关键字重复,而unordered_set不允许重复。
各种操作与unordered_map容器相同。
17.8 priority_queue (优先队列)
优先级队列相当于一个有权值的单向队列queue,在这个队列中,所有元素是按照优先级排列的。
底层容器可以用deque和list。
各种操作与queue容器相同。
17.9 stack(栈)
底层容器可以用deque和list
18、STL算法
- STL算法函数实现了很多通用的功能
- 建议复习《函数指针和回调函数》和《重载括号运算符》
- 重点在对技术的理解和使用
示例:
#include<iostream>
#include<vector>
#include<list>
#include<algorithm>
using namespace std;
template<typename T>
void Zsshow(const T& no) //张三的个性化表白函数
{
cout << "亲爱的" << no << "号:我是撒子。\n";
}
template<typename T>
void czs
{
public:
void operator()(const string no) {
cout << "亲爱的" << *it << "号:我是撒子。\n";
}
}
int main()
{
vector<int>bh = { 5,8,2,6,9,3,1,7 }; //存放超女编号的容器
//list<string>bh = { "05","08","02","06","09","03","01","07" }; //存放超女编号的容器
//写一个函数,在函数中遍历容器,向超女表白,表白的方法可自定义
//foreach(bh); //不支持向部分超女表白
foreach(bh.begin(), bh.end(), zsshow<int>); //支持vector的元素为其它数据类型,也不支持其它容器
foreach(bh.begin(), bh.end(), czs<int>());
}
STL提供了很多处理器的函数模板,它们的设计是相同的,有以下特点:
- 用迭代器来标识需要处理数据的区间
- 返回迭代器放置处理数据的结果(如果有结果)
- 接受一个函数对象参数(结构体模板),用于处理数据(如果需要)
示例:(有bug)
#include<iostream>
#include<vector>
#include<list>
#include<algorithm>
#include<functional>
using namespace std;
template<typename T>
bool compasc(const T& left, const T& right) { //用于升序
return left < right;
}
template<typename T>
class _less
{
public:
bool operator()(const T & left,const T & right) { //仿函数,用于升序
return left < right;
}
};
template<typename T>
bool compdesc(const T& left, const T& right) { //用于降序
return left > right;
}
template<typename T>
class _greater
{
public:
bool operator()(const T& left, const T& right) { //仿函数,用于降序
return left < right;
}
};
template<typename T,typename compare>
void bsort(const T first, const T last,compare comp) //冒泡排序
{
while (true)
{
bool bswap = false; //本轮遍历已交换过元素的标识,true-交换过;false-未交换过
for (auto it = first;;)
{
auto left = it; //左边的元素
it++;
auto right = it; //右边的元素
if (right == last)break; //表示it1已经是最后一个元素了
//if (*left > *right) //如果左边的元素比右边大,交换它们的值
//如果comp()返回true,left排在前面(升序),否则right排在前面(降序)
if (comp(*left, *right) == true)continue;
//交换两个元素的值
auto tmp = *right;
*right = *left;
*left = tmp;
bswap = ture; //一轮遍历已交换过元素的标识
}
if (bswap == false)break; //如果在for循环中不曾交换过元素,说明全部的元素已有序
}
}
int main()
{
//vector<int>bh = { 5,8,2,6,9,33,1,7 }; //存放超女编号的容器
list<string>bh = { "05","08","02","06","09","03","01","07" }; //存放超女编号的容器
//bsort(bh.begin(), bh.end(), compasc<string>); //普通函数(升序)
//bsort(bh.begin(), bh.end(), compdesc<string>); //普通函数(升序)
bsort(bh.begin(), bh.end(), _less<string>()); //仿函数(升序) //普通函数(升序)
bsort(bh.begin(), bh.end(), _greater<string>()); //仿函数(降序) //普通函数(升序)
for (auto val : bh)
cout << val << " ";
cout << endl;
}
一、函数对象
很多STL算法都使用函数对象,也叫函数符夺(functor),包括函数名、函数指针和仿函数。
C语言只能用函数指针,C++建议用仿函数,因为仿函数的功能更强大
函数符的概念:
1)生成器(generator):不用参数就可以调用的函数符。
2)一元函数(unary function):用一个参数可以调用的函数符。
3)二元函数(binary function):用两个参数可以调用的函数符。
改进的概念:
1)一元谓词(predicate):返回bool值的一元函数。
2)二元谓词(binary predicate):返回bool 值的二元函数。
二、预定义的函数对象
STL定义了多个基本的函数符,用于支持STL的算法函数。
包含头文件:#include< functional>
运算符和相应的函数符表(百度上查)
| 运算符 | 函数符 |
|---|---|
| + | plus |
| - | minus |
| * | multiplies |
| < | less |
| > | greater |
| / | divides |
| % | modulus |
| - | negate |
| == | equal_to |
| != | not_equal_to |
| >= | greater_equal |
| <= | less_equal |
| && | logical_and |
| || | logical_or |
| ! | logical_not |
三、算法函数
STL将算法函数分成四组:
1)非修改式序列操作:对区间中的每个元素进行操作,这些操作不修改容器的内容。
2)修改式序列操作:对区间中的每个元素进行操作,这些操作可以容器的内容(可以修改值,也可以修改排列顺序)。
3)排序和相关操作:包括多个排序函数和其它各种函数,如集合操作。
4)通用数字运算:包括将区间的内容累积、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数。通常,这些都是数组的操作特性,因此vector是最有可能使用这些操作的容器。
前三组在头文件#include< algorithm2>中,第四组专用于数值数据,在#include< numeric>中。
详见《C++Primer plus》,第六版,从886页开始。
四、学习要领
1)如果容器有成员函数,则使用成员函数,如果没有才考虑用STL的算法函数。
2)把全部的算法函数过一遍,知道大概有些什么东西。
3)如果打算采用某算法函数,一定要搞清楚它的原理,关注它的效率。
4)不要太看重这些算法函数,自己写一个也就那么回事。
5)不是因为简单,而是因为不常用。
五、常用函数
1)for_each()遍历
template<classInputIterator,class Function>
Function for_each(InputIterator first, InputIterator last,Function f);
for_each()函数将函数对象f用于[first,last]区间中的每个元素,它也返回f。
2)find()遍历
template<class InputIterator, class T
InputIterator find(InputIterator first, InputIterator last,Const T& value);
find()函数返回一个迭代器,该迭代器指向区间[first,last]中第一个值为value的元素;如果没有找到这样的元素,则返回last。
3)find_if()遍历
template<class InputIterator,class Predicate>
InputIterator find_if(InputIterator first, InputIterator last,Predicate pred);
find_if()函数返一个迭代器,该迭代器指向[first,last]区间中第一个对其调用函数对象pred(*i)时结果为true的元素;如果没有找到这样的元素,则返回last。
4)find_not_if(遍历
template<class InputIterator, class Predicate>
InputIterator find_if_not(InputIteratar first, InputIterator last,Predicate pred);
find_if_not()函数返一个迭代器,该选代器指向[first,last]区间中第一个对其调用函数对象pred(*i)时结果为false的元素;如果没有找到这样的元素,则返回last。
5)sort()排序
template< class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
template<class RandomAccessIterator,class Compare>
void sort(RandomAccessIterator first, RandomAccessIterator last,Compare comp);
sort()函数将[first,last)区间按升序进行排序,排序时使用值类型的<运算符进行比较。第一个版本使用<来确定顺序,而第二个版本使用比较对象comp。
STL的 sort 算法,数据量大时采用QuickSort(快速排序),分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort的递归调用带来过大的额外负荷,就改用InsertSort(插入排序)
如果递归层次过深,还会改用HeapSort(堆排序)。
适用于数组容器vector、string、deque(list容器有sort成员函数,红黑树和哈希表没有排序的说法)
6)二分查找
二分法搜索组中的算法假设区间是经过排序的。这些算法只要求正向迭代器,但使用随机迭代器时,效率最高。
(1) lower_bound()
template<class ForwardIterator, class T>
ForwardIterator lower_bound(Forwardlterator first, ForwardIterator last,const T& value);
template<class ForwardIterator,class T,class Compare>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last,const T& value, Compare comp) ;
lower_bound()函数在排序后的区间(first,last)中找到第一个这样的位置,即将value插入到它前面时不会破坏顺序。它返回一个指向这个位置的选代器。第一个版本使用<来确定顺序,而第二个版本使用比较对象comp
(2)upper_bound( )
template<class ForwardIterator, class T>
ForwardIterator upper_bound(Forwardlterator first, Forwardlterator last,const T& value);
template<class ForwardIterator, class T, class Compare>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last,const T& value, Compare comp);
upper_bound()函数在排序后的区间(first,last)中找到最后一个这样的位置,即将value插入到它前面时不会破坏顺序。它返回一个指向这个位置的迭代器。第一个版本使用<来确定顺序,而第二个版本使用比较对象comp。
(3) equal_range()
template<class ForwardIterator, class T>
pair<Forwardlterator, ForwardIterator> equal_range(ForwardIterator first, Forwardlterator last,const T& value);
template<class ForwardIterator, class T, class Compare>pair<ForwardIterator, ForwardIterator> equal_range(ForwardIterator first,Forwardlterator last,const T& value,Compare comp) ;
equal_range()函数在排序后的区间(first,last)区间中找到这样一个最大的子区间(it1,it2),即将value 插入到该区间的任何位置都不会破坏顺序。该函数返回一个由it1和it2组成的pair。第一个版本使用<来确定顺序,第二个版本使用比较对象comp。
(4) binary_search()
template<class ForwardIterator,class T>
bool binary_search(ForwardIterator first, ForwardIterator last,const T& value);
template<class Forwardlterator, class T, class Compare>bool binary_search(ForwardIterator first, ForwardIterator last,const T& value,Compare comp);
如果在排序后的区间[first,last]中找到与value 等价的值,则binary_search()函数返回 true,否则返回false。第一个版本使用<来确定顺序,而第二个版本使用比较对象comp。
注意:前面说过,使用<进行排序时,如果a<b和b<a都为false,则a和b等价。对于常规数字来说,等价意味着相等;但对于只根据一个成员进行排序的结构来说,情况并非如此。因此,在确保顺序不被破坏的情况下,可插入新值的位置可能不止一个。同样,如果使用比较对象comp进行排序,等价意味着comp(cb)和 comp(b,a)都为 false(这是“如果a不小于b,b也不小于a,则a与b等价”的统称)。
18.1 find_if算法
template<class InputIterator,class Predicate>InputIterator find_if(InputIterator first, InputIterator last,Predicate pred);
示例:
#include<iostream>
#include<vector>
#include<list>
#include<algorithm> //STL算法函数头文件
using namespace std;
//在这种场景意义不大,但演示了一种给回调函数传递参数的方法
template<typename T>
bool zsshow(const T& no,const T & in_no) //张三的个性化表白函数
{
if (no != in_no)return false;
cout << "亲爱的" << no << "号:我是撒子。\n";
return true;
}
template<typename T>
class czs //张三的个性化表白仿函数
{
public:
bool operator()(const T& no,const T&in_no) {
if (no != in_no)return false;
cout << "亲爱的" << no << "号:我是傻孩子。\n";
return true;
}
};
template<typename T1, typename T2,typename T3>
T1 findif(const T1 first, const T1 last, T2 pfun,T3 in_no)
{
for (auto it = first; it != last; it++)
if (pfun(*it,in_no) == true)return it; //用迭代器调用函数对象
return last;
}
int main()
{
vector<int>bh = { 5,8,2,6,9,33,1,7 }; //存放超女编号的容器
//list<string>bh = { "05","08","02","06","09","03","01","07" }; //存放超女编号的容器
//写一个函数,在函数中遍历容器,向超女表白,表白的方法可自定义
//foreach(bh); //不支持向部分超女表白
auto it1=findif(bh.begin(), bh.end(), zsshow<int>,2); //第三个参数是模板函数
if (it1 == bh.end())cout << "查找失败。\n";
else cout << "查找成功:" << *it1 << endl;
auto it2=findif(bh.begin(), bh.end(), czs<int>(),33); //第三个参数是仿函数
if (it2 == bh.end())cout << "查找失败。\n";
else cout << "查找成功:" << *it2 << endl;
}
19、智能指针
普通指针的不足
- new和new[] 的内存需要用delete和delet[]释放
- 程序员的主观失误,忘了或漏了释放
- 程序员也不确定何时释放
普通指针的释放
- 类内的指针,在析构函数中释放。
- C++内置数据类型,如何释放?
- new出来的类,本身如何释放?
智能指针的设计思路
- 智能指针是类模板,在栈上创建智能指针对象。
- 把普通指针交给智能指针对象。
- 智能指针对象过期时,调用析构函数释放普通指针的内存。
智能指针的类型
- auto_ptr是C++98的标准,C++17已弃用。
- unique_ptr、shared_ptr和weak_ptr是C++11标准的。
19.1 unique_ptr(独享)
unique_ptr 独享它指向的对象,也就是说,同时只有一个unique_ptr 指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
**包含头文件: ** #include< memory>
template <typename T, typename D = default_delete< T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept; //不可用于转换函数。
~unique_ptr() noexcept;
T& operator* () const; //重载*操作符
T* operator->()const noexcept; // 重载->操作符。
unique_ptr(const unique_ptr &) = delete; //禁用拷贝构造函数。
unique_ptr& operator=(const unique_ptr &) = delete; //禁用赋值函数。
unique_ptr(unique_ptr &&) noexcept; //右值引用。
unique_ptr& operator=(unique_ptr &&)noexcept; // 右值引l用。
//....
private:
pointerptr; //内置的指针。
}
第一个模板参数T:指针指向的数据类型。
第二个模板参数D:指定删除器,缺省用delete释放资源。
测试类AA的定义:
class AA
{
public:
string m_name;
AA() { cout << m_name<<"调用构造函数 AA0。\n";}
AA(const string & name): m_name(name){cout <<"调用构造函数 AA("<<m_name <<")。\n";}
~AA() { cout <<m_name <<"调用了析构函数~AA("<<m_name<<")。Vn"; }
};
一、基本用法
示例:
#include<iostream>
#include <vector>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AAO。\n"; }
AA(const string& name) :m_name(name) { cout << "调用构造函数AA(" << m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
int main()
{
AA* p = new AA("西施");
unique_ptr<AA> pu1(p);
cout << "m_name=" << (*pu1).m_name << endl;
cout << "m_name=" << pu1->m_name << endl;
cout << "m_name=" << (*p).m_name << endl;
cout << "m_name=" << p->m_name << endl;
}
1)初始化
方法一:
unique_ptr< AA>p0(newAA("西施")); //分配内存并初始化。
方法二:
unique_ptr< AA>p0=make_unique< AA>("西施"); //C++14标准。
方法三:(不推荐)
AA*p=new AA("西施"));unique_ptr< AA> p0(p); //用已存在的地址初始化。
2)使用方法
- 智能指针重载了* 和->操作符,可以像使用指针一样使用unique_ptr。
- 不支持普通的拷贝和赋值。
AA*p=new AA("西施"));
unique_ptr< AA> pu2 = p; //错误,不能把普通指针直接赋给智能指针。
unique_ptr< AA>pu3=newAA("西施"); //错误,不能把普通指针直接赋给智能指针。
unique_ptr< AA> pu2 = pu1; //错误,不能用其它unique_ptr拷贝构造。
unique_ptr< AA> pu3;
pu3 = pu1; //错误,不能用=对unique_ptr进行赋值。
- 不要用同一个裸指针初始化多个unique_ptr。
- get()方法返回裸指针。
- 不要用unique_ptr管理不是new分配的内存
3)用于函数的参数
- 传引用(不能传值,因为unique_ptr没有拷贝构造函数)
- 裸指针(不推荐)
4)不支持指针的运算(+、-、++、-)
二、更多技巧
1)将一个unique_ptr 赋给另一个时,如果源unique_ptr 是一个临时右值,编译器允许这样做;如果源unique_ptr 将存在一段时间,编译器将禁止这样做。一般用于函数的返回值。
unique_ptr< AA> p0;
p0=unique_ptr< AA>(newAA("西瓜"));
2)用nullptr 给unique_ptr赋值将释放对象,空的unique_ptr==nullptr。
3)release(释放对指针的控制权,将unique_ptr置为空,返回裸指针。(可用于把unique_ptr传递给子函数,并且在子函数中释放对象)
4)std::move(可以转移对指针的控制权。(可用于把unique_ptr传递给子函数,并且在子函数中释放对象)
5)reset()释放对象。
pp.reset(); //释放pp对象指向的资源对象。
pp.reset(nullptr); // 释放pp对象指向的资源对象
pp.reset(newAA("bbb"); // 释放pp指向的资源对象,同时指向新的对象。
6)swap(交换两个unique_ptr的控制权。
7)unique_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
8)unique_ptr不是绝对安全,如果程序中调用exit(退出,全局的unique_ptr可以自动释放,但局部的unique_ptr 无法释放。
9)unique_ptr提供了支持数组的具体化版本。
数组版本的unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
// unique_ptr<int[]> parr1(new int[3]); //不指定初始值
unique_ptr<int[]> parr1(new int[3]{ 33,22,11 }); // 指定初始值。
cout <<"parr1[0]="<< parr1[0] << endl;
cout <<"parr1[1]="<<parr1[1] <<endl;
cout <<"parr1[2]="<<parr1[2] << endl;
unique_ptr<AA[]>parr2(newAA[3]{string(“西施"),,string("冰冰")),string(“幂幂")}));
cout<<"parr2[0].m_name="<<parr2[0].m_name<<endl;
cout<<"parr2[1].m_name="<<parr2[1].m_name<<endl;
19.2 shared_ptr(共享)
- shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象,在内部采用计数机制来实现。
- 当新的shared_ptr与对象关联时,引l用计数增加1。
- 当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。
一、基本用法
shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数。
1)初始化
方法一:
shared_ptr< AA>p0(newAA(西施"); //分配内存并初始化。
方法二:
shared_ptr< AA>p0=make_shared< AA>("西施"); // C++11标准,效率更高。
方法三:
AA*p = new AA("西施");
shared_ptr< AA>p0(p); //用已存在的地址初始化。
方法四:
shared_ptr<AA>p0(newAA("西施"));
shared_ptr<AA>p1(p0); //用已存在的shared_ptr初始化,计数加1。
shared_ptr<AA>p1=p0; //用已存在的shared_ptr初始化,计数加1。
2)使用方法
- 智能指针重载了*和->操作符,可以像使用指针一样使用shared_ptr。
- use_count()方法返回引l用计数器的值。
- unique()方法,如果use_count()为1,返回true,否则返回false。
- 支持普通的拷贝和赋值,左值的shared_ptr的计数器将减1,右值shared_ptr的计算器将加1。left=right
- get()方法返回裸指针。
- 不要用同一个裸指针初始化多个 shared_ptr。
- 不要用shared_ptr管理不是new分配的内存。
3)用于函数的参数
二、更多细节
1)将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
2)用nullptr给shared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr。
3)release()释放对原始指针的控制权,将 unique_ptr 置为空,返回裸指针。
4)std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr。
5)reset()改变与资源的关联关系。
pp.reset();//解除与资源的关系,资源的引用计数减1。
pp.reset(newAA("bbb"));// 解除与资源的关系,资源的引I用计数减1。关联新资源。
6)swap()交换两个 shared_ptr 的控制权。
void swap(shared_ptr<T>&_Right);
7)shared_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
8)shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放。
9)shared_ptr提供了支持数组的具体化版本。
数组版本的shared_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
10)shared_ptr的线程安全性:
- shared_ptr的引l用计数本身是线程安全(引l用计数是原子操作)。
- 多个线程同时读同一个shared_ptr对象是线程安全的。
- 如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁
11)如果unique_ptr能解决问题,就不要用shared_ptr。unique_ptr的效率更高,占用的资源更少。
19.3 智能指针的删除器
- 在默认情况下,智能指针过期的时候,用delete原始指针;释放它管理的资源。
- 程序员可以自定义删除器,改变智能指针释放资源的行为。
- 删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针。
示例:
#include<iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA(){cout<<m_name<<"调用构造函数AAO。\n";}
AA(const string& name) : m_name(name) { cout << "调用构造函数AA(" << m_name << ")。 \n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
void deletefunc(AA* a) { //删除器,普通函数。
cout << "自定义删除器(全局函数)。\n";
delete a;
}
struct deleteclass { //删除器,仿函数。
void operator()(AA* a) {
cout << "自定义删除器(仿函数)。\n";
delete a;
}
};
auto deleterlamb = [](AA* a) { // 删除器,Lambda表达式。
cout << "自定义删除器(Lambda)。\n";
delete a;
};
int main()
{
shared_ptr<AA>pa1(new AA("西施a"));// 用缺省的删除器。
shared_ptr<AA>pa1(new AA("西施a"),deletefunc);// 删除器,普通函数。
//shared_ptr<AA>pa2(new AA("西施b"),deleteclass());// 删除器,仿函数。
//shared_ptr<AA>pa3(new AA("西施c"),deleterlamb);//删除器,Lambda表达式。
//unique_ptr<AA,decltype(deletefunc)*>pu1(new AA("西施1"),deletefunc);
//unique_ptr<AA, deleteclass>pu2(new AA("西施2"),deleteclass());
//unique_ptr<AA, decltype(deleterlamb)>pu3(new AA("西施3"),deleterlamb);
}
19.4 weak_ptr
一、shared_ptr存在的问题
shared_ptr 内部维护了一个共享的引引用计数器,多个 shared_ptr 可以指向同一个资源。
如果出现了循环引用的情况,引用计数永远无法归0,资源不会被释放。
示例:(有bug)
#include<iostream>
#include <memory>
using namespace std;
class AA
{
public:
string m_name;
AA(){cout<<m_name<<"调用构造函数AA()。\n";}
AA(const string& name) : m_name(name) { cout << "调用构造函数AA(" << m_name << ")。 \n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
weak_ptr<BB>m_p;
};
class BB
{
public:
string m_name;
BB() { cout << m_name << "调用构造函数AA()。\n"; }
BB(const string& name) : m_name(name) { cout << "调用构造函数BB(" << m_name << ")。 \n"; }
~BB() { cout << "调用了析构函数~BB(" << m_name << ")。\n"; }
weak_ptr<AA>m_p;
};
int main()
{
shared_ptr<AA>pa = make_shared<AA>("西施a");
shared_ptr<BB>pb = make_shared<BB>("西施b");
pa->m_p = pb;
pb->m_p = pa;
}
二、weak_ptr 是什么
- weak_ptr是为了配合 shared_ptr而引l入的,它指向一个由 shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引l用计数。
- 不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。
- weak_ptr更像是shared_ptr的助手而不是智能指针。
三、如何使用weak_ptr
weakptr没有重载 ->和 *操作符,不能直接访问资源。
有以下成员函数:
1)operator=(); //把shared_ptr或weak_ptr赋值给weak_ptr。
2)expired(); //判断它指资源是否已过期(已经被销毁)
3)lock(); // 返回 shared_ptr,如果资源已过期,返回空的 shared_ptr
4)reset(); //将当前weak_ptr指针置为空。
5)swap(); //交换
weak_ptr不控制对象的生命周期,但是,它知道对象是否还活着。
用lock(函数把它可以提升为shared_ptr,如果对象还活着,返回有效的shared_ptr,如果对象已经死了,提升会失败,返回一个空的shared_ptr。
提升的行为(lock()是线程安全的。
20、文件操作
- 在C++中,文件操作属于技术的应用,不是基本语法
- 数据持久化的两种方式:文件和数据库
20.1 写入文本文件
文本文件一般以行的形式组织数据。
包含头文件: #include< fstream>
类: ofstream (output file stream)
示例:
#include<iostream>
#include<fstream>//ofstream类需要包含的头文件。
using namespace std;
int main()
{
//文件名一般用全路径,书写的方法如下:
// 1)"D:\data\txt\test.txt" //错误。
// 2) R"(D:\data\txt\test.txt) //原始字面量。
// 3)"D:\\data\\txt\\test.txt" //转义字符。
// 4) "D:/tata/txt/test.txt" //把斜线反着写。
// 5)"/data/txt/test.txt" //Linux系统采用的方法。
string filename = R"(D:\\data\\txt\\test.txt)";
//char filename[]=R"(D:\\data\\txt\\test.txt)";
//创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::out缺省值:会截断文件内容。
// ios::trunc截断文件内容。(truncate)
// ios::app不截断文件内容,只在文件未尾追加文件。(append)
//ofstream fout(filename);
//ofstream fout(filename, ios:out);
//ofstream fout(filename, ios:trunc);
//ofstream fout(filename, ios:app);
ofstream fout;
fout.open(filename, ios::app);
//ofstream fout(filename);//创建文件输出流对象。
//fout.open(filename);// 打开文件,如果文件不存在,则创建它;如果文件已存在,则截断其内容。
//判断打开文件是否成功。
// //失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fout.is_open()== false)
{
cout << "打开文件"<<filename<<"失败。\n";return 0;
}
//向文件中写入数据。
fout << "西施|19|极漂亮\n";
fout << "冰冰|22|漂亮\n";
fout << "幂幂 | 25丨—般\n";
fout.close(); // 关闭文件,fout对象失效前会自动调用
cout << "操作文件完成。\n";
}
20.2 读取文本文件
包含头文件: #include< fstream>
类: ifstream
ifstream打开文件的模式(方式):
对于ifstream,如果文件不存在,则打开文件失败。
ios::in 缺省值:会截断文件内容。
示例:
#include<iostream>
#include<fstream>//ifstream类需要包含的头文件。
#include<string>//getline()函数需要包含的头文件
using namespace std;
int main()
{
//文件名一般用全路径,书写的方法如下:
// 1)"D:\data\txt\test.txt" //错误。
// 2) R"(D:\data\txt\test.txt) //原始字面量。
// 3)"D:\\data\\txt\\test.txt" //转义字符。
// 4) "D:/tata/txt/test.txt" //把斜线反着写。
// 5)"/data/txt/test.txt" //Linux系统采用的方法。
string filename = R"(D:\\data\\txt\\test.txt)";
//char filename[]=R"(D:\\data\\txt\\test.txt)";
//创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::in 缺省值。
//ifstream fin(filename);
//ifstream fout(filename, ios::in);
ifstream fin;
fin.open(filename, ios::in);
//ofstream fout(filename);//创建文件输出流对象。
//fout.open(filename);// 打开文件,如果文件不存在,则创建它;如果文件已存在,则截断其内容。
//判断打开文件是否成功。
// //失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fin.is_open()== false)
{
cout << "打开文件"<<filename<<"失败。\n";return 0;
}
//第一种方法
//string buffer; //用于存放从文件中读取的内容
//读取文件内容的代码
//文本文件一般以行的方式组织数据
//while (getline(fin, buffer)) { cout << buffer << endl; }
//第二种方法。
//char buffer[101]; //存放从文件中读取的内容。
//注意:如果采用ifstream.getline(),一定要保证缓冲区足够大。
//while (fin.getline(buffer, 100)) { cout << buffer << endl; }
//第三种方法
string buffer;
while (fin >> buffer) { cout << buffer << endl; }
fin.close(); // 关闭文件,fin对象失效前会自动调用
cout << "操作文件完成。\n";
}
20.3 写入二进制文件
一、文件操作
- 写文件:把内存中的数据转移到磁盘文件中
- 读文件:把磁盘文件中的数据转移到内存中。
- 内存和硬盘都是存储设备,本质上没有区别。
二、文本文件和二进制文件
- 文本文件:存放的是字符串,以行的方式组织数据,每个字节都是有意义的符号。
- 二进制文件:存放的不一定是字符串,以数据类型组织数据,内容要作为一个整体来考虑,单个字节没有意义。
三、写入二进制文件
二进制文件以数据块的形式组织数据,把内存中的数据直接写入文件。
包含头文件:#include< fstream>
类:ofstream(output file stream)
ofstream打开文件的模式(方式):
对于ofstream,不管用哪种模式打开文件,如果文件不存在,都会创建文件。
- ios::out 缺省值:会截断文件内容。
- ios::trunc 截断文件内容。(truncate)
- ios::app 不截断文件内容,只在文件未尾追加文件。(append)
- ios::binary 以二进制方式打开文件。
文本和二进制方式的区别:
1)在windows平台下,文本文件的换行标志是"r\n"。“
2)在windows平台下,如果以文本方式打开文件,写入数据的时候,系统会将"\n"转换成"\r\n";读取数据的时候,系统会将"\r\n"转换成"\n"。如果以二进制方式打开文件,写和读都不会进行转换。
操作文本文件和二进制文件的一些细节:
1)在 windows 平台下,文本文件的换行标志是"\r\n"。
示例:
#include<iostream>
#include<fstream>//ifstream类需要包含的头文件。
using namespace std;
int main()
{
ofstream fout("D:/data/txt/bbb.txt", ios::binary); //打开文件。
fout<<"abcde"<<endl; //写入文件。
fout.close(); //关闭文件。
}
2)在linux平台下,文本文件的换行标志是"\n"。
3)在windows平台下,如果以文本方式打开文件,写入数据的时候,系统会将"\n"转换成"\r\n";读取数据的时候,系统会将"\r\n"转换成"\n"。如果以二进制方式打开文件,写和读都不会进行转换。
4)在Linux平台下,以文本或二进制方式打开文本文件,系统不会做任何转换。
5)以文本方式读取文件的时候,遇到换行符停止,读入的内容中没有换行符;以二制方式读取文件的时候,遇到换行符不会停止,读入的内容中包括换行符(换行符被视为数据)。
6)在实际开发中,从兼容和语义考虑,一般:
- a)以文件方式打开文本文件,用行的方法操作它b)以二进制方式打开二进制文件;用数据块的方法操作它;
- c)以以二进制方式打开文本文件,用数据块的方法操作它(不用行hang 的方法),这种情况表示不关心数据的内容;
- d)不要以文本模式打开二进制文件,也不要用行的方法操作二进制文件,可能会破坏二进制数据文件的格式,也没有必要。(因为二进制文件中的某字节的取值可能是换行符,但它的意义并不是换行,可能是整数n个字节中的某个字节)
示例:
#include<iostream>
#include<fstream>//ofstream类需要包含的头文件。
using namespace std;
int main()
{
//文件名一般用全路径,书写的方法如下:
// 1)"D:\data\txt\test.dat" //错误。
// 2) R"(D:\data\txt\test.dat) //原始字面量。
// 3)"D:\\data\\txt\\test.dat" //转义字符。
// 4) "D:/tata/txt/test.dat" //把斜线反着写。
// 5)"/data/txt/test.dat" //Linux系统采用的方法。
string filename = R"(D:\data\txt\test.dat)";
//char filename[]=R"(D:\data\txt\test.txt)";
//创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::out 缺省值:会截断文件内容。
// ios::trunc 截断文件内容。(truncate)
// ios::app 不截断文件内容,只在文件未尾追加文件。(append)
//ios::binary 以二进制方式打开文件
//ofstream fout(filename,ios::binary);
//ofstream fout(filename, ios:out |ios::binary); //一根竖线| 是按位或运算
//ofstream fout(filename, ios:trunc |ios::binary);
//ofstream fout(filename, ios:app |ios::binary);
ofstream fout;
//“ios::app |ios::binary”,这种写法是以二进制方式打开文件,并且把写入的数据追加到文件的末尾
fout.open(filename, ios::app |ios::binary);
//ofstream fout(filename);//创建文件输出流对象。
//fout.open(filename);// 打开文件,如果文件不存在,则创建它;如果文件已存在,则截断其内容。
//判断打开文件是否成功。
// //失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fout.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
//向文件中写入数据。
struct st_girl { //超女结构体。
char name[31]; //姓名。
int tno; //编号。
char memo[301]; //备注。
double weight; //体重。
}girl;
girl = { "西施",3,"中国历史第一美女。",45.8};
fout.write((const char *)& girl, sizeof(st_girl)); // 写入第一块数据。
girl={"冰冰",8,"也是个大美女哦。",55.2};
fout.write((const char*)&girl, sizeof(st_girl)); //写入第二块数据。
fout.close(); // 关闭文件,fout对象失效前会自动调用
cout << "操作文件完成。\n";
}
20.4 读取二进制文件
一、二进制文件的格式
- 二进制文件的格式多样,由业务需求决定
- 程序员自定义的二进制文件格式,只有程序员自己才知道。
- 通用的二进制文件格式:mp3(音乐)、mp4(视频)、bmp(位图)、jpg(图片)、png(图片)
二、文本文件VS二进制文件
- 文件文件:由可显示的字符组成,方便阅读(解码),占用的空间比较多。
- 二进制文件:由比特0和1组成,组织数据的格式与文件用途有关,不方便阅读(解码)。为了节省存储空间,还可能采用压缩技术。为了保证数据安全,也可能采用加密技术。
三、读取二进制文件
包含头文件:#include< fstream>
类:ifstream
ifstream打开文件的模式(方式):
对于 ifstream,如果文件不存在,则打开文件失败。
ios::in 缺省值。
ios::binary 以二进制方式打开文件。
示例:
#include<iostream>
#include<fstream>//ifstream类需要包含的头文件。
using namespace std;
int main()
{
//文件名一般用全路径,书写的方法如下:
// 1)"D:\data\bin\test.txt" //错误。
// 2) R"(D:\data\bin\test.txt) //原始字面量。
// 3)"D:\\data\\bin\\test.txt" //转义字符。
// 4) "D:/tata/bin/test.txt" //把斜线反着写。
// 5)"/data/bin/test.txt" //Linux系统采用的方法。
string filename = R"(D:\data\bin\test.dat)";
//char filename[]=R"(D:\data\bin\test.dat)";
//创建文件输出流对象,打开文件,如果文件不存在,则创建它。
// ios::in 缺省值。
// ios::binary 以二进制方式打开文件
//ifstream fin(filename,ios::binary);
//ifstream fout(filename, ios::in | ios::binary);
ifstream fin;
fin.open(filename, ios::in | ios::binary);
//ofstream fout(filename);//创建文件输出流对象。
//fout.open(filename);// 打开文件,如果文件不存在,则创建它;如果文件已存在,则截断其内容。
//判断打开文件是否成功。
// //失败的原因主要有:1)目录不存在;2)磁盘空间已满;3)没有权限,Linux平台下很常见。
if (fin.is_open() == false)
{
cout << "打开文件" << filename << "失败。\n"; return 0;
}
//二进制文件以数据块(数据类型)的形式组织数据。
struct st_girl {//超女结构体。
char name[31];// 姓名。
int no;//编号。
char memo[301];//备注。
double weight;// 体重。
}girl;
while (fin.read((char*)&girl, sizeof(girl)))
{
cout << "name=" << girl.name << ", no=" << girl.no
<< ", memo=" << girl.memo << ", weight="
<< girl.weight << endl;
}
fin.close(); // 关闭文件,fin对象失效前会自动调用
cout << "操作文件完成。\n";
}
20.5 随机存取
一、fstream类
fstream 类既可以读文本/二进制文件,也可以写文本/二制文件。
fstream类的缺省模式是ios::in|ios::out。如果文件不存在,则创建文件,但是,不会清空文件原有的内容
普遍的做法是:
1)如果只想写入数据,用ofstream;如果只想读取数据,用ifstream;如果想写和读数据,用nfstream,这种情况不多见。不同的类体现不同的语义。
2)在Linux平台下,文件的写和读有严格的权限控制(需要的权限越少越好)
二、文件的位置指针
对文件进行读/写操作时,文件的位置指针指向当前文件读/写的位置。
很多资料用“文件读指针的位置”和“文件写指针的位置”,容易误导人。不管用哪个类操作文件,文件的位置指针只有一个。
1)获取文件位置指针
ofstream类的成员函数是tellp();ifstream类的成员函数是tellg();fstream类两个都有,效果相同。
std:streampos tellp();
std:streampos tellg();
2)移动文件位置指针
ofstream类的函数是seekp();ifstream类的函数是seekg();fstream类两个都有,效果相同。
方法一:
std::istream & seekg(std::streampos _Pos);
fin.seekg(128); //把文件指针移到第128字节。
fin.seekp(128); //把文件指针移到第128字节。
fin.seekp(ios::beg) //把文件指针移动文件的开始。
fin.seekp(ios::end) //把文件指针移动文件的结尾。
方法二:
std::istream & seekg(std::streamoff _Off,std::ios::seekdir _Way);
在ios中定义的枚举类型:
enum seek_dir{beg,cur,end)}; // beg-文件的起始位置;cur-文件的当前位置;end-文件的结尾位置。
fin.seekg(30, ios::beg); //从文件开始的位置往后移30字节。
fin.seekg(-5, ios::cur); //从当前位置往前移5字节。
fin.seekg( 8, ios::cur); //从当前位置往后移8字节。
fin.seekg(-10, ios::end); //从文件结尾的位置往前移10字节。
三、随机存取
随机存取是指直接移动文件的位置指针,在指定位置读取/写入数据。
20.6 缓冲区和流状态
一、文件缓冲区
- 文件缓冲区(缓存)是系统预留的内存空间,用于存放输入或输出的数据。
- 根据输出和输入流,分为输出缓冲区和输入缓冲区。
- 注意,在C++中,每打开一个文件,系统就会为它分配缓冲区。不同的流,缓冲区是独立的。
- 程序员不用关心输入缓冲区,只关心输出缓冲区就行了。
- 在缺省模式下,输出缓冲区中的数据满了才把数据写入磁盘,但是,这种模式不一定能满足业务的需求
输出缓冲区的操作:
1)flush()成员函数
直接刷新缓冲区。
2)endl
换行,然后刷新缓冲区。
3)unitbuf
fout << unitbuf;
设置fout输出流,在每次操作之后都进行刷新缓冲区。
4)nounitbuf
fout<<nounitbuf;
设置fout输出流,让fout回到缺省的缓冲方式。
二、流状态
流状态有三个:eofbit、badbit、failbit,取值:1-设置;或0-清除。当三个流状成都为0时,表示一切顺利,good(成员函数返回true。
1)eofbit
当输入流操作到达文件未尾时,将设置eofbit。
eof()成员函数检查流是否设置了eofbit。
2)badbit
无法诊断的失败破坏流时,将设置badbit。(例如:对输入流进行写入;磁盘没有剩余空间)。
bad()成员函数检查流是否设置了badbit。
3)failbit
当输入流操作未能读取预期的字符时,将设置failbit(非致命错误,可挽回,一般是软件错误,例如:想读取一个整数,但内容是一个字符串;文件到了未尾)I/O失败也可能设置failbit。
fail()成员函数检查流是否设置了failbit。
4)clear()成员函数清理流状态。
5)setstate()成员函数重置流状态
21、C++异常
- 异常的理念看似有前途,但实际的使用效果并不好。
- 编程社区达成的一致意见是,最好不要使用这项功能,
- C++98引l入异常规范,C++11已弃用。
一、异常的语法
1)捕获全部的异常
try
{
//可能抛出异常的代码
//throw异常对象;
}
catch (...)
{
//不管什么异常,都在这里统一处理。
}
2)捕获指定的异常
try
{
//可能抛出异常的代码。
//throw异常对象;
}
catch(exception1 e)
{
//发生exception1异常时的处理代码。
}
catch(exception2e)
{
//发生exception2异常时的处理代码。
}
在 try 语句块中,如果没有发生异常,执行完try 语句块中的代码后,将继续执行 try 语句块之后的代码;如果发生了异常,用throw抛出异常对象,异常对象的类型决定了应该匹配到哪个catch语句块,如果没有匹配到catch语句块,程序将调用 abort()函数中止。
如果try 语句块中用 throw 抛出异常对象,并且匹配到了catch 语句块,执行完catch 语句块中的代码后,将继续执行catch 语句块之后的代码,不会回到try 语句块中。
如果程序中的异常没有被捕获,程序将异常中止。
示例:
#include<iostream>
using namespace std;
int main(int argc, char* argv[])
{
try
{
//可能抛出异常的代码。
int ii = 0;
cout << "你是一只什么鸟?(1 - 傻傻鸟;2 - 小v小鸟)"; //throw抛出const char*类型的异常
cin >> ii;
if(ii == 1)throw"不好,有人说我是一只傻傻鸟。"; //throw抛出int类型的异常
if (ii == 2)throw ii;
if (ii == 3)throw string("不好,有人说我是一只傻鸟"); //throw抛出string类型的异常
cout << "我不是一只傻傻鸟,哦耶。\n";
}
catch (int ii)
{
cout << "异常的类型是int=" << ii << endl;
}
catch (const char*ss)
{
cout << "异常的类型是const char*=" << ss<< endl;
}
catch (string str)
{
cout << "异常的类型是string=" << str << endl;
}
//catch(...)//不管什么异常,都在这里处理
//{
// cout << "捕获到异常,具体没管是什么异常。\n";
//}
//cout << "程序继续运行....\n"; //执行完try..catch...后,将继续执行程序中其它的代码。
}
三、异常规范
C++98标准提出了异常规范,目的是为了让使用者知道函数可能会引发哪些异常。
void func10 throw(A, B, C); //表示该函数可能会抛出A、B、C类型的异常。
void func20 throw(); //表示该函数不会抛出异常。
void func30; //该函数不符合C++98的异常规范。
C++11标准弃用了异常规范,使用新增的关键字noexcept指出函数不会引l发异常。
void func4() noexcept; //该函数不会抛出异常。
在实际开发中,大部分程序员懒得在函数后面加noexcept,弃用异常已是共识,没必要多此一举。
关键字noexcept也可以用作运算符,判断表达试(操作数)是否可能引发异常;如果表达式可能引发异常,则返回false,不则返回true
四、C++标准库异常
示例:
#include <iostream>
using namespace std;
int main()
{
//其它处理业务的代码
//分配一大块内存。
double* ptr = nullptr;
/*try {
ptr = new double[100000000000];
}
catch (bad_alloc&)
{
cout << "分配内存失败。\n";
}*/
//一般不用上面的,而是用下面的代码
ptr = new(std::nothrow) double[100000000000];
if(ptr == nullptr)cout << "分配内存失败。\n";
//其它处理业务的代码
if (ptr != nullptr)delete[]ptr;
}
·五、重点关注的异常
1)std:bad_alloc
如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(stdnothrow)选项,则返回 nullptr,不会产生异常。
2)std:bad_cast
dynamic_cast可以用于引l用,但是,C++没有与空指针对应的引l用值,如果转换请求不正确,会出现std:bad_cast异常。
3)std:bad_typeid
假设有表达式typeid(*ptr),当ptr是空指针时,如果ptr是多态的类型,将引l发std:bad_typeid异常。
六、逻辑错误异常
程序的逻辑错误产生的异常std:logic_error,通过合理的编程可以避免。
1)std:out_of_range
Defines a type of object to be thrown as exception. It reports errors that are consequence of attempt to access elements out of defined range.It may be thrown by the member functions of std:bitset and std:basic_string, by std::stoi and std::stod families of functions, and by the bounds-checked member access functions (e.g. std::vector::at and std::map::at).
2)std::length_error
3)std::domain_error
4)std::invalid_argument
示例1:
#include<iostream>
#include <vector>
using namespace std;
int main()
{
try {
vector<int>vv = { 1,2,3 }; //容器vv中只有三个元素。
vv.at(3) = 5; //将引发out_of_range异常。
}
catch (out_of_range) {
cout << "出现了out_of_range异常。\n";
}
示例2:
#include<stdexcept>
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123"; //不会抛出异常。
//string str =""; // 将抛出Invalid_argument异常。
//string str ="253647586946334221002101"; //将抛出out_of_range异常。
try {
int x = stoi(str); //把string字符串转换为整数。
cout << "x=" << x << endl;
}
catch (invalid_argument&) {
cout << " invalid_argument. \n";
}
catch (out_of_range&) {
cout << "cout of range.\n";
}
catch (...) {
cout << "something else..." << endl;
}
}
22、C++断言
一、断言
断言(assertion)是一种常用的编程手段,用于排除程序中不应该出现的逻辑错误。
使用断言需要包含头文件< cassert>或<assert.h>,头文件中提供了带参数的宏assert,用于程序在运行时进行断言。
语法:assert(表达式);
断言就是判断(表达式)的值,如果为0(false),程序将调用abort(函数中止,如果为非0(true),程序继续执行
断言可以提高程序的可读性,帮助程序员定位违反了某些前提条件的错误。
注意:
- 断言用于处理不应该发生的错误,而非逻辑上的可能会发生的错误。
- 不要把需要执行的代码放到断言的表达式中。
- 断言的代码一般放在函数/成员函数的第一行,表达式多为函数的形参。
示例:
#include <iostream>
#include <cassert>//断言assert宏需要包含的头文件。
using namespace std;
void copydata(void *ptr1,void *ptr2) //把ptr2中的数据复制到ptr1中。
{
assert(ptr1 && ptr2);//断言ptr1和ptr2都不会为空。
cout << "继续执行复制数据的代码...\n";
}
int main()
{
int ii = 0, jj = 0;
copydata(nullptr, &jj);//把ptr2中的数据复制到ptr1中。
}
二、C++11静态断言
assert宏是运行时断言,在程序运行时候才能起作用。
C++11新增了静态断言static_assert,用于在编译时检查源代码。
使用静态断言不需要包含头文件。
语法:static_assert(常量表达式,提示信息);
注意:static_assert的第一个参数是常量表达式。而assert的表达式既可以是常量,也可以是变量。
23、C++11标准
- 2011年发布
- 稳定、兼容、易用、高性能、安全、多核编程
一、long long类型
- 新增了类型longlong和unsigned longlong,以支持64位
- 在VS 中,int和long 都是4字节,longlong是8字节。
- 在Linux中,int是4字节,long 和long long 是8字节。
二、char16_t和char32_t类型
- 新增了类型char16_t和char32_t,以支持16位和32位的字符。
- 意义不大,好像没什么人用,连demo程序都找不到。
三、原始字面量
在《45、C++11的原始字面量》中有详细介绍。
四、统一的初始化(列表)
C++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表)可以用于所有内置类型和用户自定义类型。使用统一的初始化列表时,可以添加等号(=),也可以不添加:
int x={5}
double y{2.75};
short quar[5]{4,5,2,76,1};
统一的初始化列表也可以用于new表达式中:
int *ar=new int[4]{2,4,6,7};
创建对象时,也可以使用大括号(而不是圆括号)来调用构造函数:
STL容器提供了将initializerlist模板类作为参数的构造函数:
vector<int> v1(10); //把v1初始化为10个元素。
vector<int>v2{10}; //把v2初始化为1个元素,这个元素的值是10。
vector<int>v2{3,5,8}; //把v3初始化为3个元素,值分别是3、5、8。
头文件<initializer_list>提供了对模板类initializer_list的支持,这个类包含成员函数begin()和end()。除了用于构造函数外,还可以将initializer_list用于常规函数的参数:
示例:
#include<iostream>
#include <initializer_list>
double sum(std::initializer_list<double> il)
{
double total = 0;
for (auto it = il.begin(); it != il.end(); it++)
total = total + *it;
return total;
}
int main()
{
//doubletotal=sum(3.14,5.20,8);//错误,如果没有大括号,这是三个参数。
double total = sum({ 3.14, 5.20, 8 });//正确,有大括号,这是一个参数。
std::cout << "total=" << total << std::endl;
}
五、自然推导类型auto
在《二.1 自动推导类型auto》中有详细介绍。
六、decltype关键字
在《二.2.4 函数模板高级》》中有详细介绍。
七、函数后置返回函数
在《二.2.4 函数模板高级》中有详细介绍。
八、模板的别名
九、空指针nullptr
- 空指针是不会指向有效数据的指针。以前,C/C++用O表示空指针,这带来了一些问题,这样的话0既可以表示指针常量,又可以表示整型常量。
- C++11新增了关键字 nullptr,用于表示空指针;它是指针类型,不是整型类型。
- 为了向后兼容,C++11仍允许用0 来表示空指针,因此表达式 nullptr==0为true。
- 使用nullptr提供了更高的类型安全。例如,可以将0传递给形参为int的函数,但是,如果将 nullptr 传递给这样的函数,编译器将视为错误。
- 因此,出于清晰和安全考虑,请使用nullptr。
十、智能指针
在《193、智能指针unique_ptr》至《196、智能指针weak_ptr》中有详细介绍。
十一、异常规范方面的修改
在《209、C++异常》中有详细介绍。
十二、强类型枚举(枚举类)
-
传统的C++枚举提供了一种创建常量的方式,但类型检查比较低级。还有,如果在同一作用域内定义的两个枚举,它们的成员不能同名。
-
针对枚举的缺陷,C++11标准引I入了枚举类,又称强类型枚举。
-
声明强类型枚举非常简单,只需要在enum后加上关键字class。
例如:
enum e1{ red, green }; enum class e2 { red, green, blue }; enum class e3{red,green, blue, yellow }; -
使用强类型枚举时,要在枚举成员名前面加枚举名和:,以免发生名称冲突,如:e2::red,e3::blue
-
强类型枚举默认的类型为 int,也可以显式地指定类型,具体做法是在枚举名后面加上:type,type可以是除wchar_t以外的任何整型。
例如:
enum class e2:char {red, green,blue };
十三、explicit关键字
C++支持对象自动转换,但是,自动类型转换可能导致意外。为了解决这种问题,C++11引I入了éxplicit 关键字,用于关闭自动转换的特性。
在《121、自动类型转换》中有详细介绍。
十四、类内成员初始化
在类的定义中初始化成员变量。
class Girl
{
private:
int m bh=20; //年龄。
string m_name="美女"; // 姓名。
char m_xb ='X'; //性别。
public:
Girl(int bh, string name) : m_bh(bh), m_name(name) {}
};
十五、基于范围的for循环
在《184、基于范围的for循环》中有详细介绍。
十六、新的STL容器
1)array (静态数组)
array的大小是固定的,不像其它的模板类,但array有begin(和end(成员函数,程序员可以array 对象使用 STL 算法。
2)forward_list(单向链表)
- unordered_map、unordered_multimap、unordered_set、unordered_multiset (哈希表)个
十七、新的STL方法
1)C++11新增了的方法 cbegin(、cend0、crbegin(、crend(),这些方法将元素视为 const。
2)iteratoremplace(iteratorpos,...);//在指定位置插入一个元素,...用于构造元素,返回指向插入元素的迭代器。
3)更重要的是,除了传统的拷贝构造函数和赋值函数,C++11新增了移动构造函数和移动赋值函数。
十八、摈弃export
C++98新增了export关键字,C++11不再使用,但仍保留它作为关键字,供以后使用。
十九、嵌套模板的尖括号
为了避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
vector<list<int> >v1; //两个>之间必须加空格。
C++11不再这样要求:
vector<list<int>>v2; //两个>之间不必加空格。
二十、final关键字
final关键字用于限制某个类不能被继承,或者某个虚函数不能被重写。
final关键字放在类名或虚函数名的后面。
二十一、override关键字
在派生类中,把override放在成员函数的后面,表示重写基类的虚函数,提高代码的可读性。
在派生类中,如果某成员函数不是重写基类的虚函数,随意的加上override 关键字,编译器会报错。
示例:
class AA
{
public:
virtual void test()
{
cout <<"AA class...";
}
};
二十二、数值类型和字符串之间的转换
传统方法用sprintf()和snprintf()函数把数值转换为char* 字符串;用atoi()、atol()、atof()把char* 字符串转换为数值。
C++11提供了新的方法,在数值类型和 string 字符串之间转换。
1、数值转换为字符串
使用to_string()函数可以将各种数值类型转换为string字符串类型,这是一个重载函数,在头文件< string>中声明,函数原型如下:
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);
2、字符转换为串数值
在C++中,数值类型包括整型和浮点型,针对于不同的数值类型提供了不同的函数在头文件< string>中声明,函数原型如下:
int stoi( const string& str, size_t* pos = nullptr, int base = 10 );
long stol( const string& str, size_t* pos = nullptr, int base = 10 );
long long stoll( const string& str, size_t* pos = nullptr, int base = 10 );
unsigned long stoul( const string& str, size_t* pos = nullptr, int base = 10 );
unsigned long long stoull( const string& str, size_t* pos = nullptr, int base = 10 );
float stof( const string& str, size_t* pos = nullptr );
double stod( const string& str, size_t* pos = nullptr );
long double stold( const string& str, size_t* pos = nullptr );
形参说明:
str:需要要转换的 string 字符串。
pos:传出参数,存放从哪个字符开始无法继续解析的位置,例如:123a45,传出的位置将为 3。
base:若base 为 0,则自动检测数值进制:若前缀为 0,则为八进制,若前缀为 0x 或 0x,则为十六进制,否则为十进制。
注意:字符转换为串数值会抛出异常,在《209、C++异常》中有详细介绍。
示例:
string str="123a45";
size_t posint val = stoi(str, &pos, 10);
cout<<"val="<<val<<endl; //输出123
cout<<"pos="<<pos<< endl; //输出3
二十三、静态断言static_assert
在《210、C++断言》中有详细介绍。
二十四、常量表达式constexpr
const关键字从功能上来说有双重语义:只读变量和修饰常量。
示例:
void func(const int len1)
{
//len1是只读变量,不是常量。
int array[len1]={0}; //VS会报错,Linux平台的数组长度支持变量,不会报错。
const int len2 =8;
int array1[len2]={0}; // 正确,len2是常量。
}
C++11标准为了解决const关键字的双重语义问题,保留了const表示“只读”的语义,而将“常量”的语义划分给了新添加的constexpr关键字。
所以,C++11标准中,建议将const和constexpr的功能区分开,表达“只读”语义的场景用const,表达“常量”语义的场景用 constexpr。
二十五、默认函数控制=default与=delete
在C++中自定义的类,编译器会默认生成一些成员函数:
- 无参构造函数
- 拷贝构造函数
- 拷贝赋值函数
- 移动构造函数
- 移动赋值函数
- 析构函数
=default表示启用默认函数
=delete表示禁用默认函数。
示例:
#include <iostream>
using namespace std;
class Girl//超女类
{
private:
int m_bh = 20; //年龄。
string m_name = "美女"; // 姓名。
char m_xb = 'X'; //性别。
public:
//Girl0 = default; //启用默认构造函数。
// Girl(int bh, string name) : m_bh(bh), m_name(name)()
//Girl(const Girl& g) = delete; //删除拷贝构造函数。
void show() { cout << "bh=" << m_bh << ",m_name=" << m_name << endl; }
//~Girl0 = delete; //删除析构函数。
};
int main()
{
Girl g1;
//g1.show();
//Girl g2 = g1; //错误,拷贝构造函数已删除。
}
24、委托构造和继承构造
C++11标准新增了委托构造和继承构造两种方法,用于简化代码。
一、委托构造
在实际的开发中,为了满足不同的需求,一个类可能会重载多个构造函数。多个构造函数之间可能会有重复的代码。例如变量初始化,如果在每个构造函数中都写一遍,这样代码会显得臃肿。
委托构造就是在一个构造函数的初始化列表中调用另一个构造函数。
注意:
- 注意不要生成环状的构造过程。
- 一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。
二、继承构造
在C++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。在《126、如何构造基类》中有详细介绍。
C++11推出了继承构造(lnheritingConstructor),在派生类中使用using来声明继承基类的构造函数。
示例:
#include <iostream>
using namespace std;
class AA // 基类。
{
public:
int m_a;
int m_b;
//有一个参数的构造函数,初始化m_a
AA(int a) : m_a(a) { cout << " AA(int a)" << endl; }
//有两个参数的构造函数,初始化ma和mb
AA(int a, int b) : m_a(a), m_b(b) { cout << " AA(int a, int b)" << endl; }
};
class BB :public AA // 派生类。
{
public:
double m_c; using AA::AA;//使用基类的构造函数。
//有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB(int a, int b, double c) : AA(a, b), m_c(c) {
cout << "BB(int a, int b, double c)" << endl;
}
void show() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c << endl; }
};
int main()
{
//将使用基类有一个参数的构造函数,初始化m_a
BB b1(10);
b1.show();
//将使用基类有两个参数的构造函数,初始化m_a和m_b
BB b2(10, 20);
b2.show();
//将使用派生类有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB b3(10, 20, 10.58);
b3.show();
}
25、lambda 函数
lambda函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
lambda函数的特点是:距离近、简洁、高效和功能强大。
示例:[](const int&no)->void{cout<<"亲爱的”<<no<<“号:我是一只傻傻鸟。\n";}
语法:
一、参数列表
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
与普通函数的不同:
- lambda函数不能有默认参数。
- 所有参数必须有参数名。
- 不支持可变参数。
二、返回类型
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。
三、函数体
类似于普通函数的函数体。
四、捕获列表
通过捕获列表,,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。
捕获列表在书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
以下列出了不同的捕获列表的方式。
| [] | 空捕获列表。lambda不能使用所在函数中的变量 |
|---|---|
| [names] | mames是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下是值捕获,名字前加&指明是引用捕获 |
| [=] | 隐式捕获列表,采用值捕获方式。1ambda体将拷贝所使用的来自所在函数的实体都值 |
| [&] | 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用。 |
| [&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引l用捕获。identifier_list中的名字前面不能使用& |
| [=,identifier_list] | identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用& |
1)值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。
与传递参数不同,变量的值是在lambda函数创建时拷贝,而不是调用时拷贝。
例如:
size_tv1=42;
autof=[v1]{returnv1;}; //使用了值捕获,将v1拷贝到名为f的可调用对象。
v1 =0;
auto j = fO); //j为42,f保存了我们创建它是v1的拷贝。
由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值。
默认情况下,如果以传值方式捕获变量,则在lambda函数中不能修改变量的值。
2)引用捕获
和函数引l用参数一样,引l用变量的值在lambda函数体中改变时,将影响被引l用的对象。
size_t v1 = 42;
autof=[&v1 ]{returnv1;};// 引l用捕获,将v1拷贝到名为f的可调用对象。
v1 =0;
auto j = f(); //j为 0。
如果采用引l用方式捕获变量,就必须保证被引l用的对象在lambda执行的时候是存在的。
3)隐式捕获
除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
int a = 123;
auto f=[=]{cout<<a<<endl;};//值捕获
f(); //输出:123
auto f1 [&]{cout<< a++<<endl;}; //引用捕获
f1(); //输出:123(采用了后++)
cout<<a<<endl;//输出124
4)混合方式捕获
lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
混合捕获时,捕获列表中的第一个元素必须是=或&,此符号指定了默认捕获的方式是值捕获或引用捕获。
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:
int i = 10;
int j = 20;
auto f1 = [ =, &i] 0 { return j + i; };//正确,默认值捕获,显式是引用捕获
auto f2 [ =, i] 0 { return i + j; };//编译出错,默认值捕获,显式值捕获,冲突了
auto f3 =[&, &i] 0 {return i +j;};//编译出错,默认引用捕获,显式引用捕获,冲突了
5)修改值捕获的值
在lambda函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引l发编译错误。
在lambda函数中,如果希望修改值捕获变量的值,可以加mutable选项,但是,在lambda函数的外部,变量的值不会被修改。
int a = 123;
autof=[a]0mutable{cout<<++a<<endl;}; // 不会报错
cout << a<< endl; //输出:123
f(); //输出:124
cout<< a<< endl; //输出:123
6)异常说明
lambda可以抛出异常,用throw(..)指示异常的类型,用noexcept指示不抛出任何异常。
五、lambda 函数的本质
当我们编写了一一个lambda函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数
1)采用值捕获
采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
int a =10;
int b =20;
auto addfun =[=] (const int c)-> int{return a+c;};
int c = addfun(b);cout<<c<<endl;
等同于:
class Myclass
{
int m_a; //该成员变量对应通过值捕获的变量。
public:
Myclass(int a):m_a(a){}; // 该形参对应捕获的变量。
//重载了0运算符的函数,返回类型、形参和函数体都与lambda函数一致。
int operator((const int c) const
{
return a +c;
}
};
26、右值引用
一、左值、右值
在C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。
还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
C++11扩展了右值的的概念,将右值分为了纯右值和将亡值。
-
纯右值:
a)非引l用返回的临时变量;
b)运算表达式产生的结果;
c)字面常量(C风格字符串除外,它是地址)。
-
将亡值:与右值引l用相关的表达式,例如:将要被移动的对象、T&&函数返回的值、std:move(的返回值、转换成T&&的类型的转换函数的返回值。
不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。
示例:
class AA
{
int m_a;
}
AA getTemp()
{
return AA();
}
int ii = 3; // ii是左值,3是右值。
int jj = ii+8; //j是左值,ii+8是右值。
AA aa = getTemp(); //aa是左值,getTempO的返回值是右值(临时变量)
二、左值引用、右值引用
C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引l用(rvaluereference)的概念,所以C++98 中的引l用都称为了左值引用(lvaluereference)。
右值引用就是给右值取个别名,使用的符号是&&。
语法:数据类型&& 变量名=右值;
示例:
#include <iostream>
using namespace std;
class AA
{
public:
int m_a = 9;
};
AA getTemp()
{
return AA();
}
int main()
{
int&& a = 3; //3是右值。
int b = 8; //b是左值
int&& c = b + 5;//b+5是右值。
AA&& aa = getTemp(); //getTemp()的返回值是右值(临时变量)
cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "aa.m_a=" << aa.m_a << endl;
}
getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引l用重获了新生,其生命周期将与右值引l用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。
引入右值引用的主要目的是实现移动语义。
左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量行值,石信一而自在绑定石值的的候,宝量左情S像石值引电一样将右值的生命期延长,缺点是,只能读不能改
示例:
#include <iostream>
using namespace std;
class AA
{
public:
int m_a = 9;
};
AA getTemp()
{
return AA();
}
int main()
{
int a = 1;
const int& ra = a; // a是非常量左值。
const int b = 1;
const int&rb=b; //b是常量左值。
const int& rc = 1; // 1是右值。
}
总结一下,其中T是一个具体类型:
1)左值引用,使用T&,只能绑定左值。
2)右值引用,使用T&&,只能绑定右值。
3)已命名的右值引用是左值。
4)常量左值,使用月constT&,既可以绑定左值又可以绑定右值。
27、移动语义
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
移动构造函数的语法:类名(类名&&源对象){...}
移动赋值函数的语法:类名& operator=(类名&&源对象){...}
示例:
#include <iostream>
using namespace std;
class AA
{
public:
int* m_data = nullptr; //数据成员,指向堆区资源的指针。
AA() = default; //启用默认构造函数。
void alloc() { //给数据成员m_data分配内存。
m_data = new int; //分配内存。
memset(m_data, 0, sizeof(int)); // 初始化已分配的内存。
}
AA(const AA& a) { // 拷贝构造函数。
cout << "调用了拷贝构造函数。\n"; //显示自己被调用的日志。
if (m_data == nullptr)alloc(); //如果没有分配内存,就分配
memcpy(m_data, a.m_data, sizeof(int)); //把数据从源对象中拷贝过来
}
AA(AA && a) { // 移动构造函数。
cout << "调用了移动构造函数。\n"; //显示自己被调用的日志。
if (m_data != nullptr)delete m_data; //如果已分配内存,先释放掉
m_data = a.m_data; //把资源从源对象中转移过来
a.m_data = nullptr; //把源对象中的指针置空
}
AA& operator=(const AA& a){ //赋值函数。
cout << "调用了赋值函数。\n"; //显示自己被调用的日志。
if(this == &a) return *this; //避免自我赋值。
if (m_data == nullptr) alloc(); //如果没有分配内存,就分配。
memcpy(m_data,a.m_data, sizeof(int));//把数据从源对象中拷贝过来。
return *this;
}
AA& operator=(AA&& a) { //移动赋值函数。
cout << "调用了赋值函数。\n"; //显示自己被调用的日志。
if (this == &a) return *this; //避免自我赋值。
if (m_data != nullptr)delete m_data; //如果已分配内存,先释放掉
m_data = a.m_data; //把资源从源对象中转移过来
a.m_data = nullptr; //把源对象中的指针置空
return *this;
}
~AA() { //析构函数
if (m_data != nullptr) {
delete m_data; m_data = nullptr;
}
}
};
int main()
{
AA a1; //创建对象a1。
a1.alloc(); //分配堆区资源。
*a1.m_data = 3; //给堆区内存赋值。
cout << "a1.m_data=" << *a1.m_data << endl;
AA a2 = a1; //将调用拷贝构造函数。
cout << "a2.m_data=" << *a2.m_data << endl;
AA a3;
a3 = a1; //将调用赋值函数。
cout << "a3.m_data=" << *a3.m_data << endl;
auto f = [] {AA aa; aa.alloc(); *aa.m_data = 8; return aa; };// 返回AA类对象的lambda函数。
AA a4=f();//lambda函数返回临时对象,是右值,将调用移动构造函数。
cout <<"a4.m_data="<<*a4.m_data<< endl;
AA a6;
a6 = f(); //lambda函数返回临时对象,是右值,将调用移动赋值函数。
cout << "a6.m_data=" << *a6.m_data << endl;
}
注意:
1)对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
2)如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数
3)C++11中的所有容器都实现了移动语义,避免对贪有资源的对象发生无谓的拷贝
4)移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
28、完美转发
在函数模板中,可以将自己的参数“完美”地转发给其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
C++11标准引I入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。
为了支持完美转发,C++11提供了以下方案:
1)如果函数模板的参数类型为T&&,C++可以准确地判定出实际传入的实参是左值还是右值。
2)提供了模板函数std::forward< T>(参数),用于转发参数,如果参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
29、可变参数模板
可变参数模版是C++11新增的最强大的特性之一,它对参数进行了泛化,能支持任意个数、任意数据类型的参数。
示例:
#include <iostream>
#include <thread>
using namespace std;
//递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板。
void print()
{
cout << "递归终止。\n";
}
//展开参数包的递归函数模板。
template <typename T, typename ...Args>
void print(T arg, Args... args)
{
cout << "参数:" << arg << endl;//显示本次展开的参数。
//show(arg);//把参数用于表白。
//cout << "还有" << sizenf...(args) << "个参数未展开。" << endl;//显示未展开变参的个数。
print(args...); //继续展开参数
}
int main(void)
{
print("金莲", 4, "西施");
//print("冰冰",8,"西施",3);
}
1)voidpush_back(constT&value); //在容器的尾部追加一个元素。
2)void emplace_back(...); //在容器的尾部追加一个元素,..用于构造元素。C++11
3)iteratorinsert(iteratorpos,constT&value); //在指定位置插入一个元素,返回指向插入元素的迭代器。
4)iteratoremplace(iteratorpos,.); //在指定位置插入一个元素,..用于构造元素,返回指向插入元素的迭代器。C++11
5)iteratorinsert(iteratorpos,iteratorfirst,iteratorlast); //在指定位置插入一个区间的元素,返回指向第一个插入元素的迭代器。
6)void pop_back(); //从容器尾部删除一个元素。
29、时间操作chrono库
C++11提供了chrono模版库,实现了一系列时间相关的操作(时间长度、系统时间和计时器)。
头文件:#include< chrono>
命名空间:std:chrono
一、时间长度
duration模板类用表示一段时间(时间长度、时钟周期),如:1小时、8分钟、5秒。
duration的定义如下:
template<class Rep,class Period = std:ratio<1,1>>
class duratione
{
....
};
为了方便使用,定义了一些常用的时间长度,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于std::chrono命名空间下,定义如下:
using hours = duration<Rep, std::ratio<3600>> //小时
using minutes = duration<Rep, std::ratio<60>> //分钟
using seconds = duration<Rep> //秒
using milliseconds =duration<Rep, std::milli> //毫秒
using microseconds = duration<Rep, std::micro> //微秒
using nanoseconds =duration<Rep, std::nano> //纳秒
注意:
- duration模板类重载了各种算术运算符,用于操作duration对象。
- duration模板类提供了count()方法,获取duration对象的值。
示例:
#include <iostream>
#include <chrono>// chrono库的头文件。
using namespace std;
int main()
{
chrono::hours t1(1); // 1小时
chrono::minutes t2(60); //60分钟
chrono::seconds t3(60 * 60); // 60*60秒
chrono::milliseconds t4(60 * 60 * 1000); // 60*60*1000毫秒
//chrono::microseconds t5(60 *60 *1000 *1000); //警告:整数溢出。
//chrono::nanoseconds t6(60*60*1000*1000*1000); //警告:整数溢出。
chrono::seconds t7(1); //1秒
chrono::milliseconds t8(1000);//1000毫秒
chrono::microseconds t9(1000 *1000);//1000*1000微秒
chrono::nanoseconds t10(1000*1000 *1000);// 1000*1000*1000纳秒
if(t1 == t2)cout <<"t1==t2\n";
if(t1 == t3)cout <<"t1==t3\n";
if(t1 == t4)cout <<"t1==t4\n";
//获取时钟周期的值,返回的是int整数。
cout << "t1=" << t1.count() << endl;
cout << "t2=" << t2.count() << endl;
cout << "t3=" << t3.count() << endl;
cout << "t4=" << t4.count() << endl;
//获取时钟周期的值。
cout <<"t7="<<t7.count()<< endl;
cout <<"t8="<< t8.count()<< endl;
cout <<"t9="<< t9.count()<< endl;
cout <<"t10="<< t10.count()<< endl;
}
二、系统时间
system_clock类支持了对系统时钟的访问,提供了三个静态成员函数:
//返回当前时间的时间点。
static std::chrono::time_point<std::chrono::system_clock> now() noexcept;
//将时间点time_point类型转换为std:time_t类型。
static std::time_t to_time_t(const time_point& t) noexcept;
//将std::time_t类型转换为时间点time_point类型。
static std::chrono::system_clock::time_point from_time_t(std::time_t t)noexcept;
示例:
#define_CRT_SECURE_NO_WARNINGS // localtime()需要这个宏。
#include<iostream>
#include <chrono>
#include<iomanip>// put_time0函数需要包含的头文件。
using namespace std;
int main()
{
//静态成员函数chrono::system_clock:now()用于获取系统时间。(C++时间)
chrono::time_point<chrono::system_clock> now = chrono::system_clock::now();
//静态成员函数chrono:system_clock:to_time_t()把系统时间转换为time_t。(UTC时间)
time_t t_now = chrono::system_clock::to_time_t(now);
//std::localtime()函数把time_t转换成本地时间。
//localtime不是线程安全的,Vs用localtime_s代替,Linux用localtime_r代替。
tm* tm_now = std::localtime(&t_now);
//格式化输出本地时间。
std:cout << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S") << std::endl;
std:cout << std::put_time(tm_now, "%Y-%m-%d") << std::endl;
std:cout << std::put_time(tm_now, "%H:%M:%S") << std::endl;
std:cout << std::put_time(tm_now, "%Y%m%d%H%M%S") << std::endl;
}
三、计时器
steady_clock类相当于秒表,操作系统只要启动就会进行时间的累加,常用于耗时的统计(精确到纳秒)。
示例:
#include <iostream>
#include <chrono>
using namespace std;
int main()
{
//静态成员函数chrono:steady_clock:nowO获取开始的时间点。
chrono::steady_clock::time_point start = chrono::steady_clock::now();
//执行一些代码,让它消耗一些时间。
cout << "计时开始......\n";
for (int ii = 0; ii < 1000000; ii++) {
//cout<<"我是一只傻傻鸟。\n";
}
cout << "计时完成......\n";
//静态成员函数chrono::steady_clock::now()获取结束的时间点。
chrono::steady_clock::time_point end = chrono::steady_clock::now();
// 计算消耗的时间。
auto dt = end -start;
cout << "耗时:" << dt.count() << "纳秒(" << (double)dt.count() / (1000 * 1000 * 1000) << "秒)";
}
30、C++11线程
一、线程的基本概念
- 了解基本概念即可,不要纠结概念,不要深入语法,查资料(百度)
- 在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样
- C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台
二、创建线程
头文件:#include< thread>
线程类:std::thred
构造函数:
1)thread() noexcept;
默认构造函数,构造一个线程对象,不执行任何任务(不会创建/启动子线程)
2)template<class Function,class...Args>
explicit thread(Function &&fx,Args&&...args);
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数。
3)thread(thread&&other)=delete;
删除拷贝构造函数,不允许线程对象之间的拷贝
4)thread(thread&&other)noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象
赋值函数:
thread& operator=(thread && other) noexcept;
thread& operator=(const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
注意:先创建的子线程不一定跑得最快。
示例:(有bug)
#include <iostream>
#include<thread> //线程类的头文件
#include <windows.h>//Sleep0函数需要这个头文件。
using namespace std;
//普通函数
void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); //休眠1秒
}
}
//仿函数
class mythread1
{
public:
void operator()(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); //休眠1秒
}
}
};
//类中有静态成员函数
class mythread2
{
public:
static void func(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
::Sleep(1000); //休眠1秒
}
}
};
//类中有普通成员函数
class mythread3
{
public:
void func()(int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
::Sleep(1000); //休眠1秒
}
}
};
int main()
{
//用普通函数创建线程
thread t1(func, 3, "我是好人");
thread t2(func, 8, "你好呀!");
//用lambda函数创建线程
auto f = [](int bh, const string& str) {
for (int ii = 1; ii <= 10; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
Sleep(1000); //休眠1秒
}
};
//thread t3(f, 3, "我是坏人!");
//用仿函数创建线程
thread t4(mythread1(), 3, "我是坏人!");
//用类的静态成员函数创建线程
thread t5(mythread2::func, 3, "我是傻逼");
//用类的普通成员函数创建线程
mythread3 myth; //必须先创建类的对象,必须保证对象的生命周期比子线程要长
thread t6(&mythread3::func, &myth, 3, "我是人"); //第二个参数必须填对象的this指针
cout << "任务开始。\n";
for (int ii = 0; ii < 10; ii++) {
cout << "执行任务中...\n";
Sleep(1000);//假设执行任务需要时间。
}
cout << "任务完成。\n";
//t1.join(); //回收线程t1的资源
//t2.join(); //回收线程t2的资源
//t3.join(); //回收线程t3的资源
//t4.join(); //回收线程t4的资源
//t5.join(); //回收线程t5的资源
t6.join(); //回收线程t6的资源
}
浙公网安备 33010602011771号