C++程序语言设计复习提纲
缓考个人复习用,详细部分在个人未细化的知识点。
大部分资料来源于《C++程序设计》刘瑞芳主编。
集中在数据类型:
- 数组
- 普通数组
- 多维数组
- 字符数组
- 枚举(enum)、结构(struct)类型、联合类型(union)
- 类和对象(重点)
函数:
- 函数调用
- 参数传递(值传和地址传)
- 变量类型(四类)
- 内联(inline)和重载(overload)
指针(重点):
指针的基本使用
动态内存
引用(类型名 &变量名)
指针与函数
- 指针与字符串
- 指针/引用作为参数
指针与数组/结构体
const的一点知识
课后习题答案:https://blog.csdn.net/Slatter/article/details/93309541 # 第一章:C++语言概述
程序设计
面向过程的程序设计又称为结构化程序设计,一般强调的是3种基本结构:顺序、选择和循环结构。
类和对象:封装性、继承性、多态性
对象是类的实例化,类是对象的抽象。
在面向过程的程序设计中,C++的程序模块是以函数的形式实现的,在面向对象的程序设计中,C++程序模块是以类的形式实现的。
程序开发过程
1.源程序
2.目标程序 .obj
源程序经过翻译加工后所生成的程序,一般用机器语言表示
3.可执行程序.exe
目标程序和所用的其他资源进行链接生成的可以直接运行的程序
翻译程序(3类):
-汇编程序:源程序翻译为机器语言形式的目标程序
-编译程序:将高级语言编写的源程序翻译成机器语言形式的目标程序
-解释程序:将使用高级语言编写的源程序翻译成机器指令
5.链接程序:
对汇编程序或编译程序生成的目标程序与所需的其他资源进行链接生成可执行文件的程序。
注释://注释一行说明,/* */,注释一段说明
编译预处理:“#”开头的代码
内存模型
一个程序执行时一定会先复制到内存,然后由CPU逐句读取来执行。
每个储存单元有1个字节的大小(8 bit),每个内存单元有一个唯一的地址。一般来说,地址是顺序编址。
CPU访问内存,进行取/存,读/写内存中的信息
内存分区使用:
代码区:被编译成机器码的程序在执行时会被复制此
数据区:程序中的变量和常量会被存储到此
数据区又分为:
栈区:存放程序函数的局部变量。先入后出,自动释放
全局变量区和静态变量区: 存放长期数据
常量区一般是存放字符串常量的地方
堆区:在程序设计过程中申请的内存空间,这些空间应该在内存中释放。
全局变量和静态变量位于同一个区域,先定义的放在低地址,后定义的放在高地址。局部变量则相反,先定义的放在高地址,后定义的放在低地址。
C++程序由注释、编译预处理、程序主体组成。
一个C++程序需要经过编辑、编译和链接,才能产生可执行文件。
第二章:基本数据类型与表达式
词法记号和标识符
关键词:
auto bool break case catch char class const
const_cast continue default delete do double dynamic_cast else
enum explicit extern false float for friend goto
if inline int long mutable namespace new operator
private protected public register reinterpret_case return short signed
sizeof static static_cast struct switch template this throw
true try typedef typeid typename union unsigned using
virtual void volatile while
标识符:C++的标识符是大小写敏感的
数据类型
基础数据类型:整型、字符型、实型、逻辑型
自定义数据类型:数组、指针、引用、空类型、结构、联合、枚举、类
分类 | 名称 | 标识 |
---|---|---|
整型 | int | |
字符型 | char | |
基础数据类型 | 实型 | float、double |
逻辑型 | bool | |
数组 | type[] | |
指针 | type* | |
自定义数据类型 | 引用 | type& |
空类型 | void | |
结构 | struct | |
联合 | union | |
枚举 | enum | |
类 | class |
修饰基本数据类型的关键词:
short 短整型 2字节(16 bit)
long 修饰int和double
unsigned 修饰char、short和int,表示该数据类型为无符号数
signed 与上面相反
类型 | 长度(字节) | 取值范围 |
---|---|---|
char/signed char | 1 | -128~127 |
unsigned char | 1 | 0~255 |
short int/short | 2 | -32768~32767 |
unsigned short int | 2 | 0~65535 |
int/signed int | 4 | -231~231-1 |
unsigned int | 4 | 0~232-1 |
long/long int | 4 | -231-231-1 |
unsigned long | 4 | 0~232-1 |
float | 4 | -3.4x1038~3.4x1034 |
double | 8 | -1.7x10308~1.7x10308 |
long double | 8 | -1.7x10308~1.7x10308 |
16位机下int的长度为2字节;32位机下int的长度为4字节
short和long表示的长度是固定的,因此如果需要编写可移植性好的程序,应将整型数据声明为short
变量和常量
变量
变量的实质是内存中的一个地址空间,在这个地址空间中可以进行数据的存储和读取。
变量定义语句是为变量分配存储空间
先定义或声明,后使用,且只能定义一次
typedef:声明原有数据类型的一个别名
常量
使用const定义常量
符号常量:
const 数据类型 常量名 = 常量值,必须在定义时就进行初始化
整型常量:
八进制:以数字0开头
十六进制:以0x或0X开头
整型常数默认是int类型,后缀字母L或l表示长整型,后缀字母U或u表示无符号型
实型常量:
指数:aEb的形式表示,代表ax10b。b必须是十进制数
实型常数默认为double型,可以用后缀字母f或F转换为float型,后缀L或l表示long double型
字符常量:
用单括号括起来的一个可显示字符表示字符常数,如'a'、'A'等
转义字符:以“”开头,回车,o是八进制数,h是十六进制数
在内存中,字符数据以ASCII码存储,也可以看成是单字节整数表示,所以字符数据和整型数据之间可以相互转换。
字符串常量:
由双引号括起来的字符序列,例如"abc","Hello World",除了存储包含的字符,还需要存储一个结束符'\0'
逻辑常量:
0和1
运算符和表达式
表达式由运算符、操作数(常量、变量)和分隔符号组成的序列,并总能返回一个值作为表达式的结果
三元运算符:? :
取余运算:%
只要有一个操作数是浮点数,除法的结果就是浮点数。
不允许对浮点数进行取余操作
前置++i:先+1,后使用
后置i++:先使用,后+1
关系表达式的结果类型为bool
除了逻辑非,逻辑运算的级别低于关系运算
&&和||为短路运算符,只要能确定逻辑表达式的结果,就不再进行运算
位运算:
按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移位(<<)、右移位(>>)
按位与:将两个操作数对应的每一位分别进行逻辑与操作
使用逻辑与可以将操作数中的若干位置0(其他位不变),或者去操作数中的若干位
1 | a=a&0376//将字符变量a的最低位置0 |
按位或:
将操作数中的若干位置1(其他位不变)
按位取反:~1=0,~0-=1
位运算的常见用法是实现掩码运算,掩码是一个位模式,从一个字中选出一组位。
例如:0xFF表示一个字的低位字节,若有int型变量x,则位运算x&0xFF生成一个由x的最低字节组成的值,而其他字节置0,这就相当于从x中选出它的最低字节。
条件运算符:表达式1?表达式2:表达式3;
如果表达式1的值为真,则返回表达式2,否则返回表达式3
数据类型转换
转换的基本原则是将精度较低、范围较小的类型转换为精度较高、范围较大的类型。
逻辑运算,非0即真
强制类型转换:(数据类型名)表达式 或 数据类型名(表达式)
强制类型转换运算符:static_cast<类型名>(表达式)
输入输出
基本输出
putchar 输出字符 putchar()
printf
基本输入
getchar getchar()
scanf scanf("%",&)
类型字符 | 含义 |
---|---|
d | 十进制数 |
o | 八进制数 |
x | 十六进制数 |
u | 无符号十进制数 |
i | 整型 |
f | 实型的小数形式 |
e | 实型的指数形式 |
g | f和e的较短形式 |
c | 字符 |
s | 字符串 |
l或h | 放在任何整数转换说明符之前,用于输入/输出long或short类型数据 |
l或L | 放在任何浮点转换说明符之前,用于输入/输出double或long double类型数据 |
标准输入输出流
cin
使用提取操作符“>>”将键盘键入的数据读入到变量中
cout
使用插入操作符“<<”
sizeof运算符
用于确定某种数据的长度,单位是字节
输出格式控制
1 | printf("%4d,OK\n",3);//控制宽度为4 |
C++的I/O流提供了操纵符
1.设置域宽 setw(int n)控制输出间隔
2.左对齐:setiosflags(ios::left) 右对齐:setiosflags(ios::right)
除了setw()操纵符外,其他操纵符一旦设置,则对后面所有的输入/输出造成影响,直到重新设置才改变格式;
3.设置填充字符
setfill(char c)来设置其他字符作为间隔的填充
4.设置浮点数的显示
C++默认输出的浮点数有效位为6位,但单独使用setprecision(int n)可以控制显示浮点数的数字个数
直接输出或设置精度为0都是输出6位有效数字
setiosflags(ios::fixed)操纵符是用定点方式表示浮点数,若不设置精度,则显示六位有效小数,总的有效数字可以超过6位
setiosflags(ios::scientific)操纵符使用质数方法显示浮点数
将setprecision(int n)和setiosflags(ios::scientific)控制指数表示法的小数位数
使用操纵符将小数截短显示后,将进行四舍五入处理
string
关系运算符实际上比较的是两个string对象的字母,即字符ASCII码的大小
文件读写
使用标准库的ifstream类和ofstream类来定义文件流对象
1 | ofstream ofile("odata.txt");//定义文件流对象ofile,并指定它与磁盘文件odata.txt关联 |
第三章:控制语句
基本控制结构
3种基本控制结构:顺序结构、选择结构和循环结构
顺序结构:按语句编写顺序执行的语句结构
选择结构:根据给定条件的真假而选择不同语句执行的语句结构
循环结构:在一定条件下重复执行指定语句的语句组,用于处理需要重复执行某些操作的问题
算法及其表示
算法:解决特定问题的方法和具体步骤。可以先用伪代码或流程图来表述算法,然后再逐渐完善
switch
1 | switch(表达式) |
表达式的值可以是字符型、整型,也可以是枚举型,其他类型不允许(实型、指针类型等)
循环结构
循环语句的主要部分是:循环控制条件、循环体和循环控制变量
while():表达式为真则执行,先判断后执行
do-while:先执行一次,再判断表达式的值
1 | do |
for:
1 | for(循环控制变量赋初值;循环条件;修改循环控制变量值) |
一般用for实现循环次数一定的问题,而用while和do-while实现循环次数事先不能确定的问题。
for语句的3个表达式种任意一个或几个可以不写,但是“;”不能省略
省略表达式1意味着循环控制变量赋初值的语句要放在for语句之前完成。
表达式1和表达式3都可以是逗号语句
1
2
3
4for(sum=0,k=1;k<=10;k++)
{
sum+=k*k;
}
break:从循环体跳出,去执行循环结构后面的语句
continue:提前结束本次循环,进入下次循环
随机数
rand()可以产生一个0~RAND_MAX之间的随机数,是伪随机数
需要在使用rand()用srand()函数为随机数序列设置种子
1 |
|
结构嵌套
if嵌套:从最内层开始,else总是与前面最近的(未曾匹配的)if配对
第四章:数组及自定义数据类型
用户自定义的数据类型和系统预定义的数据类型的地位是等价的
数组的分量称为元素,结构的分量为成员;
数组存储类型和意义相同的集合元素,结构类型的成员用不同的数据类型描述了一种实体的不同属性。
枚举类型实质是有限个整数的集合。
结构变量所占内存长度是各个成员所占的内存长度之和,每个成员都有自己的内存单元。
联合变量所占的内存长度等于最长的成员的长度,无论有多少成员,他们共用内存单元。
数组(Array)
定义数组:
类型标识符 数组名[常量表达式];
定义语句使系统给该数组分配一段连续的内存空间,数组名表示该内存空间的起始地址
元素的下标可以理解为元素存放位置相对于数组名的偏移量
数组名是一个地址常量,禁止给数组名赋值
数组所占字节数:sizeof(数组名)或N * sizeof(数组类型)
数组初始化:
类型标识符 数组名[常量表达式]={以逗号隔开的初始化值};
初始化数值不能多于数组元素,可以少于数组元素
访问数组元素
数组元素的下标表达式的结果必须是0或正整数
数组的下标值不得越界
复制两个数组可以使用memcpy函数:
1 | int a[5],b[5]; |
字符数组
1 | char chArray[]="Hello World!"; |
字符串中每个字符占用1个字节,加上字符串常量最后的结束符,数组需要的字节数比显示的字符数要多一个。
初始化字符数组
用双引号内的字符串常量初始化字符数组
1
char array[]={"Hellow"};
用字符常量来初始化字符数组
1
char array[]={'H','e','l','l','o',\0};
字符数组的赋值
使用strcpy(字符数组1,字符串2),即字符串复制函数,将字符串2复制到字符数组1
1 | char str1[10]="",str2[]="hello"; |
字符数组1要能够容纳被复制的字符串
不能用赋值语句将一个字符串常量或字符数组直接给字符数组赋值,如str1="Hello";
多维数组
n维数组:类型标识符 数组名称标识符[常量表达式1][常量表达式2]...[常量表达式n]
多维数组初始化时需要使用嵌套的括号
枚举类型(enum)
枚举类型定义
enum 新的数据类型名称{变量值名称}
1 | enum weekday{sun,mon,tue,wed,thu,fri,sat}; |
sun,mon等称为枚举元素或枚举常量
枚举变量定义及使用
1 | enum weekday{sun,mon,tue,wed,thu,fri,sat}; |
变量day的取值范围为类型定义
1 | day=sat; |
在类型定义后,枚举元素按常量处理,不能对它们赋值,如:sum=0
枚举元素具有默认值,它们依次为:0,1,...。例如,sun=0,mon=1,...
枚举类型可以进行关系运算,但不能进行其他运算
也可以在类型声明时另行指定枚举元素的值
1
enum weekday{sun=7,mon=1,tue,wed,thu,fri,sat};
枚举值可以直接赋给整型变量,但整数值不能直接赋给枚举变量;若需要将整数值赋给枚举变量,则需要进行强制类型转换,如day=weekday(3)
结构类型(struct)
结构类型的定义和初始化
1 | struct 结构类型名 |
例如:
1 | struct student |
大括号括起的内容称为结构的成员。
结构变量的定义和使用
1 | studnet s1; |
要访问结构的成员,需要使用圆点操作符”.“,它是双目操作符,左边的操作数是结构变量名,右边的操作数是结构的成员名,引用形式为:结构变量名.成员名
1 | cout<<s1.name; |
结构变量的初始化
在结构变量定义的同时设置初始值
1
student s2={20041118,”Li Li“,18,90,”Xi Tu Cheng Lu 10“};
在程序中,单独给结构变量的各个成员赋值
1
2s1.num=20041118;
strcpy(s1.name,”Li Li“);
结构变量的赋值运算
属于同一结构类型的各个变量之间可以相互赋值,即使结构体内有数组类型的数据成员。
1 | student s1,s2; |
不同结构的变量不允许相互赋值,即使这两个变量可能有同样的成员。
结构变量代表一个完整的内存空间,复制结构变量就是将这块内存空间整体复制到另一个位置。
联合类型(union)
1 | union 联合类型名 |
联合类型变量定义的语法形式为:联合类型名 联合变量名
在某时刻,只能使用多个成员的其中之一
引用形式:联合变量名.成员名
1 | union uarea |
新的数据类型uarea属于联合类型,它有3个成员,这三个成员共用内存空间,分配给uarea类型的变量ux的内存空间如下图所示
如果在主程序这样写:
ux.s_data=10;
ux.l_data=20;
最终结果是:20覆盖了先存入的10;
联合类型可以不声明名称,称为无名联合,常用作结构类型的内嵌成员
联合型变量的特点:
- 同一段内存用来存放几种不同类型的成员,但在某一时刻只能存放其中一种,而不能同时存放几种
- 联合变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后,原有的成员会失去作用
- 联合变量的地址和它的各个成员的地址是同一地址
- 不能对联合变量名赋值,也不能在定义时初始化
- 不能用联合变量作为函数参数或返回值
字符串的输入与输出
getline(字符数组名,大小,终止处)
除了字符数组,其他类型的数组要输出数组元素的值,必须用循环语句一个元素一个元素地输出,而数组名只能代表数组的储存地址
内存空间问题
多维数组在内存中的存放
一维数组在内存中从数组名所代表的起始地址开始,按下标次序存储
二维数组在内存中从数组名所代表的起始地址开始,按行优先依次存储,数组的第i行第j列元素在内存中的起始位置相对于数组起始地址偏移了“行号 x 列数 + 列号”个int型变量空间大小
三维数组在内存中从数组名所代表的起始地址开始,按页、行、列依次存储,即按使数组元素最右边下标值最快变化来存储,数组的第k页第i行第j列元素在内存中的起始位置相对于数组起始地址偏移了“页号 x(行号 x 列数)+行号x列数+列号”个int型变量空间大小
二维数组实际上是一维数组的一维数组;
枚举类型的内存空间
枚举元素的默认值都是整数,计算机处理数据中把枚举类型按整型(int)对待
结构类型的内存空间
某个结构类型的变量所占的存储空间是结构中所有成员所占空间的总和(按字节计)
在实际系统中会存在结构变量空间对齐问题,对于32位机,如果某个成员所占的空间不是4的倍数,系统会将其调整为4的倍数,使得结构变量所占空间一定是4的倍数,所以结构变量占用空间经常会超过数据成员应该占用空间的总和
考虑到对齐的要求,系统会将所占的空间不是4的倍数的成员空间调整为4的倍数:将sex成员调整为4字节,将addr成员调整为32字节,总共占用68字节。
第五章:函数
概述
函数是具有一定功能又独立使用的相对独立的代码段。
函数由接口和函数体构成,函数的接口包括:函数名、函数类型和形式参数表;
1 | int max(int a,int b) |
函数体由于实现算法。
代码函数化的主要目的:
- 实现模块化设计,将项目分为小模块分别实现
- 软件复用,使用现有函数能实现的功能不必重新定义新的代码
- 避免程序代码的重复书写
自定义函数概述
将程序中多处使用的、实现一定功能的特定代码端定义成函数,这样的函数称为自定义函数
在函数调用过程中,把实现函数调用的函数称为调用函数(主调函数)被调用的函数称为被调函数。
库函数
数学库函数
函数定义中声明的所有变量都是局部变量,只在定义它们的函数中有效。
为实现函数之间的信息交换,多数函数需要有一定内容的形式参数表,这些形式参数也是局部变量。
函数的定义
一个C++程序可以由一个主函数和若干子函数构成。主函数main()是程序执行的开始点,由主函数调用子函数,子函数还可以继续调用其他子函数。
调用其他函数的函数称为主调函数,被其他函数调用的函数称为被调函数。
定义函数
每一个函数都是一个具有一定功能的语句模块,函数定义的语法形式:
1 | 返回值类型 函数名(形式参数表) |
函数名是这个独立代码段(函数体)的外部标识符,代表这个代码段在内存的起始地址。
函数的形式参数表:
(类型1 形式参数1,类型2 形式参数2,...,类型n 形式参数n)
其中,“类型”是各个形式参数的类型标识符,“形式参数”为各个形式参数的标识符
形式参数表从参数的类型、个数、排列顺序上规定了主调函数和被调函数之间信息交换的形式。
函数返回值类型规定了函数返回给主调函数的值的类型,也被称为函数类型。
由return语句返回的值的类型必须与函数返回值的类型一致。
如果函数声明时没有写返回值类型,将默认为int类型。
C++不允许函数嵌套定义。
函数原型
函数原型声明语法形式:
1 | float maximum(float x,float y,float z); |
函数原型声明中的形式参数标识符(x,y,...)可以不同于函数定义时形式参数表中的形式参数标识符,但参数的类型、个数以及形式参数的先后次序必须与函数定义时的一致;
函数声明应该写在所有函数定义之外,表明这些函数都可以被位于原型声明之后的所有函数调用。
C++常用的库头文件:
程序中包含头文件,需要用编译预处理指令include,语法形式为:
1 |
return语句
当被调函数只需要将一个数值返回给主调函数时,使用return语句返回最为合适。
函数的调用
通常采用的调用方式:函数语句、函数表达式和函数参数
函数语句
具体语句:
1
函数名(实际参数表);
实际参数表的类型、个数、排列顺序必须与被调函数声明的形式参数表一一严格对应。
函数表达式
1
2变量名=函数名(实际参数表);
变量名=带有函数调用的表达式;函数参数
将函数调用写在另一次函数调用的实际参数的位置
实际是将函数的返回值作为下次调用的实际参数
1
m=max(de,max(a,b,c));
全局变量与局部变量
局部变量
在函数体内定义的变量和函数的形式参数,它们只能在本函数内使用,不能被其他函数访问。
局部变量能够随其所在函数被调用而被分配内存空间,也随其所在函数调用结束而消失(释放内存空间),所以使用局部变量能够提高内存利用率。
由于局部变量只能被其所在的函数访问,这种变量的数据安全性也比较好。
全局变量
在函数外部定义的变量就是全局变量。
全局变量能够被位于其定义位置之后的所有函数(属于本源文件的)共用,即全局变量的作用域是整个源文件。
全局变量在程序执行的整个过程中,始终位于全局数据区内固定的内存单元。如果程序没有初始化变量,系统会将其初始化为0。
作用域
程序中标识符的作用域也就是标识符起作用的范围。
从标识符起作用的范围上划分,作用域主要分为全局作用域和局部作用域两种。
从标识符在程序中所处位置来划分,作用域又可分为块作用域、函数作用域、类作用域和文件作用域。
- 块作用域:在程序块内定义的标识符具有块作用域。从变量定义起至本块结束。
- 函数作用域:函数作用域是指标识符的作用域为函数,从标识符定义开始到函数结束。在函数定义中,任何程序块以外所定义的变量具有函数作用域。
- 文件作用域:即全局作用域 ,作用域为文件范围。在源文件所有函数之外声明或定义的标识符具有文件作用域。
可见性
标识符在其作用域内,能被访问到的位置称为可见,不能被访问到的位置称为不可见。
例如:若在某个函数定义了与某个全局变量标识符相同的局部变量,则该全局变量在这个函数内不可见。
内层标识符与外层标识符同名时,内层标识符可见,外层标识符不可见。
即内层变量屏蔽外层同名变量。
如果在函数中一定要使用这个同名全局变量,可以使用全局运算符(::)指定要访问的全局变量
结构化程序设计
多文件结构
多文件结构程序:即由多个源程序分别完成不同的子功能。
在实现每个子功能时,一般可使用两个源文件:一个包含程序自定义类型、符号常量定义和函数声明等头文件*.h,一个是由实现算法的函数的*.cpp文件
编译预处理
1 |
|
如果没有定义_DEBUG_PROGRAM_H,就定义一个标识符_DEBUG_PROGRAM_H,并对头文件的其他语句进行编译;否则就忽略这个头文件中的所有语句。
按这样的结构编写头文件,可以防止头文件的重复嵌入。
使用条件编译,在调试程序时显示一些调试的信息。在调试完毕后,屏蔽编译条件,调试信息就不显示了。
1 |
|
调试完后只需要将#define_DEBUG_MODE 注释掉,调试信息就不会显示。
递归函数
递归函数的函数体内有调用函数自身的语句或通过其他函数间接调用函数自身。
递归函数可读性好,但效率低。
所有递归问题求解可分为两步:
- 化简问题的递推阶段
- 达到递归终止条件得到基本情况的结果,并逐步回推结果阶段
递归函数划分问题:函数中能处理的部分(直达已知)和函数不能处理的部分(结果未知)
对于还不能即可给出结果的部分,函数将简化问题,再调用递归函数,逐步达到已知结果。
递归函数包含以下主要部分:
- 具有更简单参数的递归调用
- 停止递归的终止条件(递归终止条件)
多数能用递归解决的问题,也能使用迭代的方式解决。
内联函数
内联函数可以减少函数调用的时空开销,一些常用短小的函数适合采用内联函数的形式
内联函数的定义形式:
1 | inline 函数类型 函数名(形式参数表) |
形式上,只需要在函数类型前加个inline即可,内联函数是函数的一种特殊形式。
系统在编译程序的时候就已经把内联函数的函数体代码插入到相应的函数调用位置,成为主调函数内的一段代码,可以直接执行,不必再转换流程控制权,所以节省了时空开销,但是使得主调函数代码变长,故一般只把短小的代码写成内联函数。
- 内联函数不能包含循环结构、switch语句
- 内联函数要先定义、后调用,不能先声明函数原型,再调用、定义
重载函数
C++允许几个功能相似的函数同名,但同名函数的形式参数必须不同,称这些同名函数为重载函数。
1 | int max(int x,int y) |
这是两个不同的函数。
各重载函数形式参数不同是指参数的个数、类型或顺序彼此不同。
编译器不以形式参数的标识符区分重载函数
1
2int max(int a,int b);
int max(int x,int y);编译器认为这是同一个函数声明两次
编译器不以函数类型区分重载函数、
1
2int max(int a,int b);
float max(int x,int y);编译器认为这是同一个函数声明两次
不应该将完成不同功能的函数写成重载函数
带默认参数值的函数
C++允许函数的形式参数有默认值。
1 | double CaircleArea(double radius=1.0) |
调用具有默认参数的函数时候,如果提供实际参数值,则采用实际参数;若不提供实际参数值,则采用默认参数值。
形式参数表中具有默认参数值的参数右边不能出现没有默认值的参数,即带有默认值的参数要在最右端
1 | int CuboidVolumn(int length=1,int width =1,int height=1);//正确 |
函数在声明时便需要指定默认参数值。
变量的存储类型和生存期
一个变量在内存中存在的时间取决于变量的存储类型
auto型
auto型变量包括函数体内部定义的局部变量、函数的形式参数,称为自动变量。
自动变量的定义一般都省略关键词auto,一般定义的变量都是自动变量。
自动变量因其所在的函数被调用而存在,随其所在函数的调用的结束而消失。
自动变量存放于栈区,不长时间占用固定内存,有利于内存资源的动态调用。
register型
寄存器型变量,定义形式为:
1 | register 类型标识符 变量标识符; |
把counter放在CPU的寄存器内存储。
访问在寄存器中的变量比访问内存中的变量速度快,但由于寄存器数量有限,如果设置过多的register变量,编译器会将这些变量按auto型局部变量处理。
extern型
即外部存储类型
如果一个文件中的函数需要使用其他文件中定义的全局变量,可以用extern关键词声明所要用的全局变量。
提供了一个多文件程序结构不同源文件共享数据的一个途径,但需要注意数据安全。
static型
定义形式:
1 | static 类型标识符 变量标识名; |
静态变量分为静态全局变量和静态局部变量。
静态变量在程序运行期间一直在静态存储区占有固定的存储空间。
静态局部变量只在其所在函数第一次被调用时进行初始化,被初始化为指定的值。若没有指定初始化值,则系统将其初始化为0。此后静态局部变量能够保持其在前一次函数调用结束后所获得的值,直到下一次函数调用被修改。
静态全局变量只能在其定义的文件中使用,不能被多文件结构程序的其他文件访问。除此之外静态全局变量在定义它的文件用法与全局变量一致。
静态全局变量的数据安全性优于普通全局变量。
生存期
一个变量在内存中存在的时间称为变量的生存期。
按生存期可以将变量分为两种:静态生存期变量和动态生存期变量
auto型和register型具有动态生存期,全局变量和静态变量具有静态生存期。
具有静态生存期的变量在程序运行期间一直存在。被初始化时,若未指定初始化值,则初始化为0.
具有动态生存期的变量取决于函数是否被调用,在函数被调用期间存在。被初始化时,若未指定初始化值,则初始化为随机数.
函数调用的执行机制
函数调用是基于函数调用工作栈实现的。
栈空间的存取原则是先进后出。
系统在栈空间为函数调用建立工作记录,在函数的工作记录中存储主调函数的断点地址、被调函数的形式参数和自动局部变量等。
当被调函数执行完成后,系统从其工作记录中取出断点地址,并将此工作记录退栈,CPU将从主调函数的断点处继续执行。
参数的传递机制
C++的函数参数传递方式分为以下两种:
- 值转递(Pass by Value)
- 地址传递(Pass by Address)
值传递
值传递:函数的形参为普通变量时,当函数被调用,系统为形参分配内存空间,并用实参值初始化形参。
值传递方式下:
- 实参和形参各自占有自己的内存空间;
- 参数传递方向只能由实参到形参;(参数传递的单向性)
- 不论被调函数对形参对任意更改,都不影响对应的实参;
地址传递
地址传递:函数的形参为指针变量时,当函数被调用,实参使用地址变量或地址常量,即传递给实参地址值。
- 数组名作为形参,只将数组的起始地址传递给了被调函数,数组的大小需要单独通过值传递的方式传给被调函数;
- 如果是多维数组名作为函数的形参,则数组的每一维的大小都需要传给被调函数;
- 多维数组名作为形参,只可以省略第一维(最左边)的大小,注意下标越界问题;
题目
函数sumarray()计算一个数组所有元素的和,其定义如下:
1 | int sumarray(int a[],int n) |
现有int a[2][3],若求数组a中所有元素的和,则对sumarray()调用正确的为(C) A.sumarray(a,6) B.sumarray(a[0],6) C.sumarray(&a[0][0],6) D.sumarray(&a,6)
在内存空间看来,
栈空间 |
---|
a[0][0] |
a[0][1] |
a[0][2] |
a[1][0] |
a[1][1] |
a[1][2] |
函数形参要求数组起始地址,所以将&a[0][0]传入
5.14 下列说法正确的是(B) A.内联函数在运行时是将该函数的目标代码插入每个调用该函数的地方 B.内联函数在编译时是将该函数的目标代码插入每个调用该函数的地方 C.类的内联函数必须在类体内定义 D.类的内联函数必须在类体外通过加关键字inline定义
第六章:指针和应用
指针
地址和指针变量
指针变量:存放地址的变量
对变量的直接访问:通过变量名对变量进行访问
对内存单元的间接访问:通过地址对变量进行访问
访问指针时,只能看到地址,只有通过这个地址才能访问地址单元的内容
指针的定义和初始化
指针定义的格式:
1 | <变量名> *变量名1,变量名2; |
定义指针变量时的“*”有如下两个含义:
- 声明变量pa1、pa2等都是指针变量
- 说明变量pa1、pa2的类型是(int )型,即指向整型的指针。它们所指定的地址单元只能存放整型数据。pch1和pch2的类型是(char )类型,它们所指定的地址单元只能存放字符。
指针变量的类型就是它所指定的地址单元中存放的数据的类型。
指针变量在声明后,变量的值是随机的,所以一般建议在定义指针时,给指针赋值为0
1 | int *val=NULL; |
指针变量必须在初始化后才可以正确使用。
指针变量的初始化:
在定义时初始化
1
2
3<类型名> *指针变量名=&变量名;//变量名和指针要同类型
char ch1='Y';
char *pch1=&ch1;在定义指针变量后,用赋值的方式初始化
没有初始化的指针变量是不可以使用的。先初始化,后使用。
指针的使用
间接引用运算符“ * ”是一元运算符,与指针变量连用,对指针所指向的内存单元进行间接访问。
使用格式:
1 | * 指针变量 |
指针可以进行的算数运算只有加减法,指针p和整数n加减的含义是相对于p的前后方偏移n个数据单元位置。
指针和指针直接相加没有意义,且不允许。
指针和指针相减得到两个指针之间的内存长度(偏移值),而不是两个地址值的具体差值。
不允许用整数减去一个指针。
指针的赋值运算一定是地址的赋值,可以对指针赋值的参量有:
- 同类型变量的地址
- 同类型已经初始化的指针变量
- 向系统申请的同类型指针的地址
不同类型的指针是不可以互相赋值的,不存在类型的自动转换。
相同类型的指针可以进行各种关系运算,两个指针相等表示它们指向同一个内存地址。
动态内存
动态内存:在程序执行时才能申请、使用和释放的内存,即存放动态数据的内存区域。
堆:存放动态内存的区域
动态内存不能通过变量名来使用,只能通过指针来使用。
使用堆内存的情况:
- 需要存储大量数据时
- 需要存储一组数,数据类型相同但数据个数在编程时不确定,在运行时才能确定
C语言的动态内存申请和释放
C语言通过凸函数mallowc()申请动态内存,通过函数free()释放动态内存。
mallowc函数原型:
1 | void *malloc(unsigned int size); |
malloc函数申请size个字节的内存空间,并返回指向所分配内存的void *类型的指针。
void *指针具有很好的通用性,可以通过类型转换赋值给任何类型的指针变量。
如果没有申请到内存空间,则返回NULL。
1 | int *pn=(int *)malloc(sizeof(int)); |
该语句按照int类型数据存储空间的大小分配了4个字节的空间,并由整型指针pn指向该内存空间。
函数free的原型为:
1 | void free(void *ptr) |
所要释放的内存由ptr指向
C++的动态内存申请和释放
动态内存申请运算符new的使用格式:new <类型名>(初值)
运算成功返回指定类型内存的地址,申请失败返回NULL指针。
一般总是将动态申请的地址赋值给一个指针。
1 | int *pi=0; |
如果申请成功,指针pi就获得了一个有效的地址,并使得*pi=10。
动态内存使用完毕后,需要用delete运算符来释放,delete的使用格式:delete <指针名>
动态内存的申请和释放应该配合使用,new和delete应该成对出现。
申请动态一维数组时,要在new表达式中加上申请数组的大小
格式:new <类型名>[表达式]
在动态申请数组时,不可以对数组进行初始化。
1 | int *piarray=0; |
这样申请得到的地址的类型仍然是(int *),只是申请了10个这样的整型数据空间
释放动态数组空间要用:delete []<指针名>;
1 | int *p1=new int; |
引用
引用是变量或者其他编程实体(如对象)的别名,因此,引用是不可以单独定义的。
变量A在内存中有自己的地址,而A的引用B实际上就是变量A,只是A的另外一个名字。
指针变量本身也有自己的地址,是可以独立存在的,而引用是不可以独立存在的。
引用的声明
引用是通过运算符&来定义的,定义格式:<类型名>&引用名=变量名;
其中,变量名是要已经定义的,并且与引用的类型相同
1 | int someInt; |
refInt就是变量someInt的引用,引用refInt和someInt具有相同的地址,对引用refInt的操作也就是对变量someInt的操作。
引用必须在声明的同时完成初始化,不可以先声明引用,再用另一个语句对它进行初始化。
下面是错误的例子:
1 | int someInt; |
引用具有以下特定:
- 引用不能独立存在,它只是其他变量的别名;
- 引用必须在声明的同时初始化;
- 引用一旦定义,引用关系就不可以更改;
- 引用的类型就是相关变量的类型,引用的使用和变量的使用相同;
引用的使用
通过引用使用/修改相关的变量。
1 |
|
在程序中真正使用引用的地方是在函数调用中:或者将引用作为函数的形式参数,或者将引用作为函数的返回值。
指针与函数
指针作为函数参数
指针和函数可以实现地址调用。
必须满足以下条件:
- 函数的形式参数是指针变量;
- 函数的实际参数是内存的地址,具体来说可以是数组名、变量的地址、用变量地址初始化的指针;
- 形参指针类型和实参地址类型必须相同
满足以上条件后,这样的函数调用在使用上具有以下特点:
- 实参传递给形参的是内存的地址,所以形参指针指向实参变量;
- 形参指针通过间接引用直接访问实参变量,包括改变实参变量的值;
- 函数调用后,可以保留对实参变量的操作结果,如果有多个实参,就可以有多个实参变量在函数调用中得到修改
这种调用方式可以实现“参数的双向传递”,即实参将变量地址传递给形参指针,,形参将变量变化的结果传递给变量;也可以称为“可以返回多个结果”。
实际上并不存在形参到实参的“返回”操作,形参指针的间接引用就是对实参变量的操作,实参变量的变化在函数调用的过程以及发生,而不是在函数执行后才发生。
数组作为函数参数就属于这种情况,现在是用指针代替数组名,属于更一般的情况。
1 |
|
如果要函数返回一个结果,直接用函数的返回值;
如果要从函数中得到多个结果,就要使用指针作为形参的地址调用
引用作为函数参数
引用的主要应用就是作为函数的形式参数
引用作为函数的形参有如下特点:
- 实际参数是相同类型的变量
- 参数传递属于地址传递
- 在函数中并不产生实际参数的副本,形式参数的引用和实际参数的变量实际上是同一个实体
- 函数对引用的操作,也是对实参变量的操作,函数调用可以改变实际参数的值
例:
1 |
|
与指针作为形式参数的相似点:
- 两者都属于地址调用:通过指针的地址调用和通过引用的地址调用;
- 两者在函数调用时都不建立实参的副本,而是对实参的数据直接进行操作;
- 指针作为形式参数需要在函数中定义指针变量,引用作为形式参数不需要新建任何实体,所以引用不需要占用新的内存,执行效率更高;
- 用引用作为形式参数,编程语句也会更简单
所以在C++中,常常使用引用作为函数的形式参数
常指针和指针常量(不考)
可以使用常指针和常引用实现对传递参数的保护。
常指针是指向常量的指针的习惯说法,就是规定指针所指向的内容不可以通过指针的间接引用改变。
常指针的定义格式:
const <类型名> *<指针名>;
1 | const int *ptint; |
其中,指针ptint的类型是(const int*)
,也就是指向一个恒定的整型数。但是这个整型数本身也许是可以改变的,只是不可以通过指针ptint的间接引用来改变。。而ptint也可以用不同的地址对它进行赋值。
常指针最常见的应用是在函数原型中,如
1 | char *strcpy(char *s1,const char *s2); |
其中,字符串复制函数中有两个参数,都是字符指针,功能是把s2指向的字符复制给s1,s2指向的字符串不要被函数修改,所以定义常指针。
类似地,也可以定义常引用,格式为:const <类型名> <引用名>&;
还有另外一种和常量有关的指针:指针常量,也就是指针本身的内容是常量,不可以改变,声明格式为:
1 | <类型名> *const <指针名>=<初值>; |
也可以写成:
1 | <类型名> const *<指针名>=<初值>; |
例如:
1 | char ch,*const ptch=&ch; |
这是,指针ptch是用ch地址初始化的常量,不可以改为其他地址,但可以通过ptch的间接引用来改变ch的值。
数组名就是一个指针常量。
常指针可以改变指针指向的地址,但不可以改变指针指向的地址的内容;
指针常量可以改变指针指向的地址内容,但不可以改变指针指向的地址;
指针函数和函数指针(不考)
指针函数:函数的返回值是指针,例如
1 | int *func01(int k); |
返回指针,实际上就是返回一个内存单元的地址。.
例子:
1 |
|
不能返回函数中局部变量的地址,这样的地址处于内存的栈区,函数结束时所占用的栈空间就释放了,回到主调函数后不能再使用该空间了。所以上面的例子中,不能在reverse函数中定义局部数组int result[6],使用堆空间是比较好的解决办法。
指针和函数有着天然的联系,函数名本身就是地址。
指针不仅可以指向变量,还可以指向函数,指向函数的指针被称为函数指针,定义的语法格式为:
1 | <类型名>(*指针名)(形参列表): |
其中,数据类型代表函数的返回值类型,形参列表是所指函数的形参列表,例如
1 | int(*fptr)(int,int); |
函数指针指向某个函数后,就可以像使用函数名一样使用函数指针来调用函数了。
因为函数名代表函数的内存地址,所以给函数指针赋值的时候,直接用函数名即可,不需要取地址运算符&。
指针与字符串
C++字符串常量是用双引号括起的字符序列,并以字符'\0'作为结束标志。
字符常量存放在内存的常量区域,有自己固定的首地址。如果将字符串常量的首地址看作指针,这种指针既是常指针,也是指针常量。即字符串的内容和首地址都是不能改变的。
处理字符串的两种方式
数组方式和指针方式
数组方式是将字符串存入字符数组后,再进行处理,一般可以声明数组时用字符串来进行初始化。
指针方式是用字符串常量来初始化一个字符指针,例如:
1 | char *string_pt="Hello World"; |
两种操作方式的区别:
数组名是指针常量,是右值,不能放在等号左边。
字符串操作函数
C++提供了大量字符串处理函数,需要包含头文件<cstring>。
功能 | 函数原型 | 返回值 | 说明 |
---|---|---|---|
字符串长度 | int strlen(const char *string); | 长度值 | '\0'不计入 |
字符串复制 | char strcpy(char s1,const char *s2); | 复制的字符串 | s1要有足够的空间 |
按字符数复制 | char strncpy(char s1,const char *s2,int n) | 复制的字符串 | s1要有足够的空间 |
字符串比较 | int strcmp(const char s1,const char s2); | <0,=0,>0 对应s1<s2,s1=s2,s1>s2 | 按字符顺序比较ACSII码值的大小 |
字符串连接 | char strcat(char s1,const char *s2); | 连接后的字符串 | s1要有足够的空间 |
多数函数是以字符指作为形式参数,源字符串都是常指针,以保护原来的数据。
指针与数组
数组名本身就是地址
通过指针访问一维数组
一维数组名就是数组的地址,一维数组名可以看做指针,具有以下特点:
指针的类型是指向数组元素的指针,因此,数组名也是数组第一个元素的地址,对于数组A来说,数组名A和&A[0]具有相同的类型和相同的值;
通过数组名的间接引用运算,如*A,就可以访问数组的第一个元素A[0];
数组名所包含的地址值是不可改变的,是指针常量;
要通过指针访问一维数组,必须先声明一个和数组类型相同的指针,并且用数组名来对指针进行初始化,例如:
1
int A[10],*pa=A;
然后就可以通过数组名或所定义的指针变量,用以下多种方式访问数组元素
数组名和下标
指针和下标,如pa[0],pa[4]
指针加偏移量的间接引用
数组名加偏移量的间接引用
指针自加后的间接引用,如*pa++,但这种方式会改变指着本身的值
但是,不允许数组名自加后的间接引用来访问数组元素,如*A++,因为数组名是常量
指针数组
若数组元素是某种类型的指针,称这样的数组为指针数组。
指针数组的声明格式:
1 | <类型> *<数组名>\[常量表达式] |
例如:
1 | char (member_name[10]); |
使用较多的是指向字符的指针:
1 |
|
指针数组作为main函数的形参(不考)
命令行参数是main函数的参数。带有命令行参数的main函数的原型是:
1 | <类型>main(int argc,char *argv\[]); |
可见,有两个命令行参数
argc:整数,存放命令行参数的数目,这个参数不需要用户输入,由程序自动统计,所统计的命令行参数包括所执行的程序名。
argv[]:指针数组,存放所输入的命令行参数。命令行参数都看做是字符串,用空格隔开,回车结束。指针数组中存放各个字符串的地址。其中argv[0]是所执行的程序名,argv[argc-1]是最后一个输入的参数字符串,argv[argc]中自动存入NULL,表示输入结束。
二维数组与指针
二维数组可以看成是一维数组的一维数组。二维数组名虽然也是地址,但是却与一维数组有不同的类型。
对于一维数组A[5],数组名A的地址就是数组第一个元素A[0]的地址。指针的类型是指向数组元素的指针,A+1就是元素A[1]的地址。
对于二维数组B[3][4],数组名B的地址,则是其中第一个一维数组B[0]的地址。指针的类型是指向一维数组的指针。B+1就是下一个一维数组B[1]的地址。
数组名B和C虽然都是指向一维数组的指针,两者还是有差别:所指向的一维数组的大小不同。
因此在定义指向一维数组的指针时,还必须指出一维数组的大小。
指向一维数组的指针的格式:
1 | <类型名>(*指针变量名)[一维数组大小]; |
例如,和上图中两个二维数组所对应的指向一维数组的指针定义如下:
1 | char (*ptchb)[4],(ptchc)[2]; |
这样定义后,ptchb就是指向一维数组B[0]的指针,ptchb+1就是指向一维数组B[1]的指针。
对于指向一维数组的指针,具有以下特征:
- 二维数组名是指向一维数组的指针,而不是指向数组元素的指针;
- 指向一维数组指针加1的结果,是指向下一个一维数组的指针。若ptchb指向一维数组B[0],ptchb+1就是指向一维数组B[1];
- 指向一维数组的指针的间接引用的结果仍然是地址(ptchb指向的内存单元内仍然是地址),即*ptchb仍然是地址,只是地址的类型变了,变为一维数组B[0]第一个元素B[0][0]的地址。*ptchb+1是B[0][0]下一个元素的地址,即B[0][1]的地址;
因为*ptchb是数组元素的地址,**ptchb就是数组元素的值。用指向一维数组指针访问数组元素的一般公式是:*(*(指针名+i)+j)是第i行第j列元素的地址:(指针名+i)是二维数组第i行的地址,*(指针名+i)是第i行第0列元素的地址,(*(指针名+i)+j)是第i行第j列元素的地址
借助于指向一维数组指针的概念,可以用单循环访问二维数组。
指针与结构体
可以定义指向结构型数据类型的指针变量。
声明了指向结构的指针后,必须对指针进行初始化。
可以将结构变量的地址赋给结构指针,使用取地址符&操作,得到结构变量的地址,这个地址是结构的第一个成员的地址
1
2
3
4
5
6
7struct student{
long num;
char name[20];
float score;
};
student stu={20041118,"Li Li",81}
student *ps=&stu使用new操作在堆中给结构指针分配空间
1
student *ps=new student;
用结构指针访问结构成员时,用箭头操作符代替原来的点操作符。
1 | cout<<ps->score; |
ps->score等价于(*ps).score。
结构体指针数组,每个元素是结构体变量的地址,用交换指针代替交换结构体变量。
链表是通过指针链接在一起的一组数据项,是一种非常有用的动态数据结构。
1 | //创建一个链表 |
main函数首先定义了头节点指针:student *head
;并申请动态内存,再存储几个学生的信息,会组成如下图的链表
void类型的指针
void类型指针声明:
1 | void *<指针名>; |
void类型指针也指向内存地址,但是不指定这个地址单元内的数据类型。
void类型指针有如下特点:
- 任何其他类型的指针都可以赋值给void指针。但是这样赋值后的void的指针类型仍然是void;
- void类型指针不可以直接赋值给任何其他类型的指针;
- 无论何时,void类型的指针都不能通过间接引用来访问内存中的数据,因为只要是数据就有类型,不存在“无类型”的数据
- 要通过void类型指针访问内存中的数据,必须进行指针类型的强制转换。
void类型一般不会独立使用,而是作为指针类型转换的中介:将某种类型的指针转换为void指针,进行具体操作后,再转换回原来的类型。
C++有一个通用的内存区域的复制函数memcpy(),它就是将某种数据类型的地址转换void指针,进行复制后再强制转换为原来的类型地址类型。该函数的原型如下:
1 | void *memcpy(void *dest,const void *src,size_t count); |
函数有3个形参:源地址指针、目的地址指针、复制字节数。两个指针都是void类型,所以可以接受任何类型的实参指针。函数返回值是void类型目的地址指针,可以赋值给任何类型的指针。
1 | char src[10]="012345678"; |
void类型指针还可以显示字符指针的内容。除了字符指针以外,指针都可以直接用cout输出地址值,但是用cout输出字符指针时,则是输出它所指向的字符串。可以将字符指针强制转换为void指针,再用cout输出。
1 | char *pch="Hello World"; |
内存泄漏和指针悬挂
内存泄漏:动态申请的内存空间没有正常释放,但是也不能继续使用
指针悬挂:让指针指向一个已经释放了的空间
关于const
const默认作用于其左边的东西,如果左边没东西,则作用于其右边的东西。
const int*
const 要作用于左边的东西,但是左边没东西,所以const修饰int成常量整型,然后*再作用于常量整型。所以这是a pointer to a constant integer(指向一个整型,不可通过该指针改变其指向的内容,但可改变指针本身所指向的地址)即常指针
int* const
这个const的左边是*,所以const作用于指针(不可改变指向的地址),所以这是a constant pointer to an integer,可以通过指针改变其所指向的内容但只能指向该地址,不可指向别的地址。即指针常量
const int* const
这里有两个const。左边的const 的左边没东西,右边有int那么此const修饰int。右边的const作用于*使得指针本身变成const(不可改变指向地址),那么这个是a constant pointer to a constant integer,不可改变指针本身所指向的地址也不可通过指针改变其指向的内容。
int const * const
这里也出现了两个const,左边都有东西,那么左边的const作用于int,右边的const作用于*,于是这个还是a constant pointer to a constant integer。
小结
指针的特点是可变性,即指针内的地址是可变的。
引用的特点是不变性,一个变量的引用只能和这个变量联系在一起。
题目
以下程序在使用指针时有没有问题?运行后是否有问题?此题存疑
1 |
|
程序在编译时没有错误。但是,存在内存泄漏问题,申请的堆内存没有释放,运行时也会出现错误。因为delete语句要释放的不是堆内存的地址;pch中现在是字符串地址,这样的地址不需要通过delete释放,也不可以通过delete释放。
1 |
|
有点意思的字符串与指针与字符数组
字符指针:指向字符型数据的指针变量。每个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。将字符串的首地址赋给字符指针,可让字符指针指向一个字符串
1 | char *ptr = "Hello"; |
字符数组:
1 | char str[10]="Hello"; |
函数strcpy:
1 | char* my_strcpy(char* dest, const char* src) //把src所指向的字符串复制到dest中 |
传入两个数组指针dest,src,返回指针指向dest首地址
定义ret指针存入dest地址 assert(断言),需引入头文件,对不符合要求的传参发出警告 while (dest++ = src++) 代码执行,src指针++,因为是后置++,先赋值后++,src首地址所指向的第一个数据传入dest中的首地址中,修改值,while判断()中值为真,即不为0;循环继续 src++ dest++ 两个指针后移,依次进行上续操作,直至src指向 ‘\0’ src为’\0’,先赋值给dest,++后,while判断,‘\0’,值为假,跳出循环 返回dest首地址
第七章:类与对象
类和对象的定义
基本概念
类代表一类事物,事物具有相应的特征和属性。
类有数据成员和成员函数。
类是一种用户自定义的数据类型,与结构体类似,但类的成员是默认private的,不可以任意访问。
类和对象具有继承和多态的特性。
类的声明
类是一组对象的抽象化模型。
声明类的语法形式:
1 | class 类名称 |
成员既可以是数据成员,也可以是成员函数的原型。
类的成员包括数据成员和函数成员,分别描述问题的属性和操作。
数据成员的声明和一般变量相同,函数成员用于描述类的对象可以进行的操作,一般在类中声明原型,在类声明之后定义函数的具体实现。
根据访问权限不同,类成员可以分为3类:
- 私有成员:只允许本类的成员函数来访问;
- 公有成员:类对外的窗口,允许外界访问;
- 保护型成员:可访问性与私有成员性质类似,差别在于继承过程中对派生类的影响不同;
默认的访问属性是private。
类的实现
类的成员函数描述的是类的行为或操作。函数的原型声明要在类的主体中,而函数的具体实现一般写在类声明之外。
定义成员函数的语法形式如下:
1 | 返回值类型 类名::成员函数名(参数表) |
通过类名和作用域操作符"::"来表示函数属于哪个类。
类的成员函数还可以有多种形态:
带默认参数值的成员函数,默认值要写在函数原型声明中,调用规则与普通函数相同;
内联成员函数:
声明方式:隐式声明和显式声明
隐式声明:在类声明时定义的成员函数都是内联函数。函数定义时没有任何的附加说明,所以称为隐式声明。
显式声明:在类声明之后定义内联函数需要用关键词inline
1
inline 返回值类型 类名::成员函数名(参数表){函数体}
成员函数的重载
成员函数可以像普通函数那样重载。类名也是成员函数名的一部分,所以一个类的成员函数即使与另一个类的成员函数同名,也不能认为是重载。
对象的定义和使用
语法形式:类名称 对象名称;
一个对象所占空间是类的数据成员所占的空间总和。类的成员函数存放在代码区,不占用栈和堆空间。
类的成员是抽象的,对象的成员才是具体的。
类声明中的数据成员一定不能具有具体的属性值,只有对象的成员才具有具体的属性值。
数据成员的访问语法形式:对象名.公有数据成员
如果是函数成员,其一般形式为:对象名.公有成员函数名(参数表)
类的作用域与可见性
类的作用域
一个类的所有成员位于这个类的作用域内,一个类的所有成员函数都能访问这个类的所有成员,C++认为一个类的全部成员是一个整体的相关部分。
类作用域是指类定义和相应的成员定义的范围,通俗地称为类的内部。在该范围内,一个类的成员函数对本类的其他成员具有无限制的访问权。
类的可见性
类名实际上是个类型名,允许类与其他类型变量或其他函数同名。
在类的内部,与类或类的成员同名的全局变量名或函数名不可见。
在一个函数内,同名的类和变量可以同时使用,都是可见的。
构造函数
类和对象的关系是简单数据类型与其变量的关系,也就是一般与特殊的关系。
C++中对象的初始化由构造函数完成,清理由析构函数完成。
析构的顺序与构造的顺序相反。
构造函数的定义
定义构造函数的一般形式为:
1 | class 类名 |
构造函数可以在类的内部实现,也可以在外部实现。
构造函数的特点:构造函数的名称和类名相同,构造函数没有返回值,构造函数一定是公有函数。
构造函数可以带默认形参值,也可以重载。
构造函数的重载
构造函数可以像普通函数那样重载,根据需要选用。
带默认值的构造函数
同普通函数相同。
默认构造函数和无参构造函数
没有定义类的构造函数时,编译器会在编译时自动生成一个默认形式的构造函数
1 | 类名::类名(){} |
一个既没有形参,也没有任何语句的函数。
只有在类中没有定义任何构造函数的情况下,才能使用默认构造函数。
无参构造函数:
1 | 类名::类名(){语句} |
程序中不能同时出现无参数构造函数和带有全部默认参数的构造函数,否则会出现编译错误。
复制构造函数
用来复制一个对象。
复制构造函数就是函数形参是类的对象的引用的构造函数。
定义一个复制构造函数的一般形式:
1 | class 类名 |
1 | complex(const complex & c)//自己定义了一个复制构造函数 |
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类对象的引用,其作用是使用一个已经存在的对象去初始化一个新的同类的对象。
复制构造函数和原来的构造函数实现了函数的重载,如果程序没有显式定义复制构造函数,系统也会默认生成一个,将成员值一一复制。
但某些情况必须显式定义一个复制构造函数。例如,当类的成员包括指针变量时,类的构造函数用new运算符为这个指针动态申请空间,如果复制时只是简单的一一复制,就会出现两个对象指向相同的堆地址,程序就会报错,这时候就必须定义复制构造函数,在其中为新对象申请新的堆空间。
析构函数
对象所占用的空间要通过析构函数来释放,函数原型是:
1 | ~类名(); |
如果程序不定义析构函数,系统也会提供一个默认的析构函数:
1 | ~类名(){} |
这个析构函数不能释放堆空间。
析构函数也是类的一个公有成员函数,没有返回值,没有形式参数。
析构函数在对象生存期即将结束的时刻由系统自动调用的。
类的析构函数不能重载,因为析构函数没有参数。
面向对象设计
类的封装性
将数据和操作数据的行为进行有机结合就是封装性。
类是属性和操作的结合体,并在定义类的属性和操作时,规定了它们的可见性。
封装有两个含义:
- 包装,将对象的全部属性和操作结合在一起
- 信息隐藏
封装性是面向对象的重要原则。对象的属性和操作的紧密结合反映了事物的静态特征和动态特征,封装的信息隐藏能力反映了事物的相对独立性。
软件工程
可靠性,成本效益好,可理解性,可维护性。
面向对象首先是一种思想,面向对象程序设计是面向对象思想在软件工程领域的全面应用。
面向对象的意义
- 模块化
- 软件复用
对象数组
数组的元素可以是自定义的类类型。
对象数组的元素是对象,不仅具有数据成员,还有函数成员,可以通过数组元素调用成员函数。
析构的顺序与构造的顺序相反。
使用对象传递函数参数
类类型可以作为函数的一个参数类型和返回值类型。
如果类的数据成员较多,需要一一复制,这时候用对象指针或对象引用的方式来传递函数参数。
对象指针和堆对象
1 | Clock c;//在栈中分配Clock型存储空间空间 |
对象指针就是用于存放对象地址的变量,可以用new在堆中给对象分配存储空间,也可以使用一个已有对象初始化对象指针。
对象指针遵循一般变量指针的规则,声明对象指针的一般语法形式为:
1 | 类名 *对象指针名; |
使用对象指针访问对象的成员,要使用“->”运算符,语法形式为:对象指针名->公有成员;
this指针
在类的外部访问类的成员必须通过对象来调用。
成员函数是如何识别不同变量属于哪个对象呢?
对象在调用成员函数时,还接收了一个地址参数,这个参数的数据类型是类名*,形式参数的名称为this,因此成员函数的原型实际上是:
1 | 类型名 函数名(类名 *this,形参1,形参2...) |
当调用时,系统会自动取对象的地址作为实际参数赋给this指针。
需要时可以显式地使用this指针。
在成员函数内部,所有对类成员的访问都可以加上隐含的前缀this->。
this指针指出了成员函数当前所操作的数据所属的对象,不同的对象调用成员函数时,this指针将指向不同的对象,也就可以访问不同对象的数据成员。
有时需要在成员函数中用*this来标识正在调用该函数的对象。
复制析构函数
当构造时从堆中为对象的成员分配存储空间,在对象生存期结束时,把堆空间释放,归还给系统,需要定义一个复制构造函数。
内部类和命名空间
把一个类的定义写在另一个类的内部,称其为内部类
1 | class AAA |
上面的代码将类Inner定义在AAA内部,此时内部类的类名全称为:AAA:Inner,使用时要用类全名:
1 | int main(){ |
内部类在使用上和普通类几乎没有区别,外部类AAA不能自由访问内部类的成员,内部类Inner也不能自由访问外部类的成员,相当于把Inner写在外面。
内部类的用途主要是为了避免类名的冲突。当发现一个类仅在局部使用时,就可以定义一个内部类。
命名空间是解决名字冲突的终极方案,语法格式为:
1 | namespace ID |
可以把很多的名字:类名、函数名、全局变量名,定义在一个命名空间ID里,以后使用ID作为前缀,ID要在整个项目里全局唯一。
在main函数中使用命名空间里的名字时,需要加上前缀,如果确定不会有名字冲突,可以使用using语句来解除前缀。
题目
类和对象的区别是什么? 一个类表示现实生活中的一类事物,是抽象的,对象是一类事物中的一个具体的个体,即对象是类的一个具体的实例,类和对象的关系相当于普遍与特殊的关系。在C++中,类是一个自定义的数据类型,对象是该数据类型的一个变量。
什么时候系统会调用复制构造函数? 复制构造函数在以下三种情况下都会被调用: 1.用类的一个对象去初始化该类的另外一个对象 2.如果函数的形参是类的对象,调用函数时,进行形参和实参结合时 3.如果函数的返回值是类的对象,函数执行完成返回调用者时
第八章:继承
继承的概念
继承是在现有的类的基础上创建新类,并扩展现有类的功能的机制,称现有的类为基类(Base Class),新建立的类为派生类(Derived Class)。
“派生”可以理解为继承的另外一种说法。类D继承了类B可以表述为类B派生出类D。若类B派生出类D1、D2...,可以说B是D1、D2...的繁华,称B为D1、D2...的基类,称D1、D2为B的派生类。
“基类-派生类”=“父类(Parent Class)-子类(Child Class)”=“超类(Superclass)-子类(Subclass)”
泛化是一个从特殊到一般的总结过程,将子类的共同特征抽象出来,得到父类。
若派生类只有一个直接基类,则称这种继承方式为单继承;若派生类有多个直接基类,则称为多继承。
如非必要,不推荐使用多继承,多继承的问题可以使用类的组合的方法来代替。
基类和派生类
如何从基类得到派生类。基类的成员会被继承到派生类中,但是这些成员在派生类的访问控制属性受到继承方式的影响,同时继承还会导致同名覆盖。
- 派生类继承了基类的所有成员,派生类对象包括基类的数据成员,也可以直接调用基类公有函数;
- 派生类对象不可以直接访问基类的私有成员;
- 派生类对象可以通过基类的公有函数访问基类的私有成员;
定义派生类
语法格式:
1 | class 派生类名: 继承方式 基类1,继承方式 基类2,...,继承方式,基类n{ |
如果仅有一个基类,那么就是单继承,否则是多继承。
继承方式是public、private和protected三者之一,不同的继承方式会影响基类成员在派生类的访问控制属性。
派生类继承了基类的所有成员,但不包括构造函数、析构函数和默认赋值运算符。
继承方式和访问控制
protected属性
派生类希望访问基类的一些成员,但仍然禁止用户代码访问这些成员,此时,需要将基类成员设置为protected访问方式。
类的对象不能访问protected属性的成员,但是派生类的成员函数可以访问基类的protected属性的成员。
- protected成员不能通过本类对象访问(在类的外部);
- protected成员可以被派生类成员函数访问(在派生类内部);
- protected成员不能通过派生类对象访问(在类的外部);
继承方式影响访问控制
派生类的成员函数对所继承的基类成员的访问控制
派生类成员函数都可以访问基类的public和protected成员,但不能访问基类的private成员
派生类对象对所继承的基类成员的访问控制
只有public继承的派生类对象可以访问基类的public成员,protected和private继承的派生类对象不能访问基类public成员
基类成员的访问属性在派生类中的变化
- 对于public继承,基类的public成员、protected成员在派生类中仍然保持原来的访问属性;
- 对于protected继承,基类的public成员和protected成员在派生类中变为protected属性;
- 对于private继承,基类的public成员、protected成员在派生类中都变为private属性;
- 不论是哪种继承方式,基类的private成员在派生类中都不可被访问;
private继承看似好像不影响派生类成员对基类public和protected成员的访问。但如果有连续两次的private继承,基类的public和protected成员在最下面一层的派生类中都将不能访问。
同名覆盖
同名覆盖:派生类修改基类的成员,是在派生类中声明了一个与基类成员同名的新成员。在派生类作用域内或者在类外通过派生类的对象直接使用这个成员名,只能访问到派生类中声明的同名新成员,这个新成员覆盖了从基类继承的新成员。
派生类的构造和析构
派生类需要定义自己的构造函数和析构函数。派生类的构造和析构函数会受到基类构造和析构函数的影响。
基类只有无参构造函数
基类具有无参构造函数,派生类又没有定义自己的构造函数,系统会自动调用基类的无参构造函数来构造派生类对象中的基类成分。
基类没有无参构造函数,派生类也不定义自己的构造函数,便会发生语法错误。
基类的构造函数一般被声明为public访问控制方式。
派生类构造函数
一般来说,派生类构造函数要初始化本类的数据成员,还要调用基类的构造函数,并为基类构造函数传递参数,完成派生类中基类成分的初始化。
派生类构造函数的形式如下:
1 | 派生类名::派生类名(基类所需的形参,本类成员所需的形参): |
“基类1(基类参数表1),基类2(基类参数表2),...,基类n(基类参数表n)”称为构造函数初始化列表,简称为初始化列表,用来调用基类构造函数以及为基类构造函数传递参数。
1 |
|
如果是单继承,派生类构造函数的形式会更简单。
派生类的析构函数
派生类不能继承基类的析构函数,需要自己的定义析构函数。派生类的析构函数只需要清理它新定义的成员,一般来说只需要清理堆区的成员。
如果没有特殊指针数据成员需要清理,可以使用由系统提供的默认析构函数。
当派生类对象消亡时,系统调用析构函数的顺序与建立派生类对象时调用构造函数的顺序正好相反,即先调用派生类的析构函数,再调用基类的析构函数。
虚基类
多继承和二义性
多继承结构中,派生类可能有多个直接基类或间接基类,可能会引起成员访问的二义性或不确定性问题。
基类base的成员要继承到派生类Fderiver1和Fderiver2,然后又继承到派生类Sderiver,即Sderiver派生类中,基类的成员有两份拷贝。因此,通过Sderiver派生类的对象访问基类的公有成员时,编译系统就不知道应该如何从两份拷贝中进行选取,只好给出"ambiguos"的错误信息,即出现了二义性。
二义性产生的原因是基类的构造函数调用了两次,调用两次构造函数所产生的基类成员都继承到派生类Sderiver,二义性也就产生了。如果基类构造函数只调用一次,这种类型的二义性就可以解决了。
虚基类
我们可以将共同基类设置为虚基类,创建派生类对象时,虚基类的构造函数只会调用1次,虚基类的成员在第三层派生类对象中就只有一份拷贝,不会再引起二义性问题。
将共同基类设置为虚基类,需要在第一级派生类时就用关键字virtual修饰说明继承关系,其语法形式为:
1 | class 派生类名: virtual 继承方式 基类名 |
在多继承情况下,虚基类关键词的作用范围和继承方式关键词相同,只对紧随其后的基类起作用。由于这一层的派生类的多个,这一层的其它派生类也需要用virtual关键词说明。
同名覆盖和重载
同名覆盖(Override):在类继承中才会出现
重载(Overload):在同一作用域范围内,由参数个数或类型不同的多个同名函数构成,可以单独出现,也可以与override现象同时出现。
一般来说,同名覆盖现象中的多个函数原型(函数类型、名字、参数)是相同的,而重载现象中多个函数原型(参数)是不同的。
转换与继承
派生类继承了基类成员,但基类成员收到访问控制的限制。
派生类的成员与基类成员到底有上面不同呢:
每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行基类的操作,这就涉及到派生类到基类的转换。这种转换包括以下三种情况:
- 派生类对象转换为基类对象
- 基类对象指针指向派生类对象
- 用派生类对象初始化基类对象的引用
派生类对象转换为基类对象
1 |
|
编译后,运行的输出结果是:
1 | TShape s x=0 y=0 |
派生类对象c赋值给基类对象s之后,基类对象s仅获取了派生类对象c中内嵌的TShape子对象的值,而派生类对象c中的数据成员r被忽略了。
换句话说,在s=c这个赋值语句中,派生类对象c被截断成了两部分:内嵌TShape子对象和派生类独有的数据成员。这种现象被称为“对象截断”。
s=c语句实际上执行了一个隐式类型转换,将TCircle对象转换为了TShape对象。建议用s=static_cast<TShape>(c)替换s=c
基类指针指向派生类对象
1 |
|
输出结果如下
1 | TCircle c x=1 y=2 r=3 |
指针变量ps的类型是基类指针,实际指向是派生类对象c的TShape内嵌对象。
换句话说,*ps就是被截断的派生类对象c的一部分。需要注意的是,ps->Show()访问的是基类的函数TShape::Show(),而不是派生类的TCircle::Show()。
用派生类对象初始化基类对象的引用
与“基类对象指针指向派生类对象”的类似,区别在于,引用只能在定义时赋值(初始化),而指针变量可以在定义之后赋值。
基类到派生不存在转换
编译器可以自动将派生类对象转换为基类对象(隐式类型转换),但是从基类到派生类的自动转换是不存在的。
原因是基类对象不包含派生类成员,若允许用基类对象给派生类对象赋值,那么就可以试图使用该派生类对象访问不存在的成员,显然会导致错误。
题目
在类的层次结构中,采用什么顺序调用构造函数?调用析构函数的顺序是什么? 构造函数的调用次序是:基类构造函数、内嵌对象的构造函数、派生类的构造函数;析构函数的调用次序与此相反