C/C++基础
在Windows,编辑C/C++源文件可以使用记事本、Notepad++或其他文本编辑器;在Linux,编辑C/C++源文件可以使用vi或者vim。
程序基本结构与运算符
第一个程序
下面是一个最简单的C程序,将这个文本保存为hello.c。
在当前目录打开命令行,运行:Bash | |
---|---|
你会在当前目录看到生成了一个hello.exe,双击hello.exe打开,会发现其一闪而过,这是因为该程序执行完任务后会直接关闭,如果想要看到输出的内容,有以下两个办法:
1.将程序改为下面内容:
2.直接在命令行进行运行,输入.\hello.exe
回车,则可在命令行看到输出:
Bash | |
---|---|
我们目前只需要在main{...}中写需要的内容,其他部分在之后进行说明。
说明
- 试着修改引号内的内容,重新运行程序,观察输出的结果。
- 删掉末尾的分号,重新运行程序,看看会发生什么。
如果你随意地删掉上面代码的一部分,就很可能会出现报错,这是因为运行的代码不符合语法。如同自然语言,编程语言也要对应的一套语法,这是本教程的重点。但语法错误不是报错的唯一原因,还可能会因为其它原因报错,我们在之后的内容讲到。
如果你的代码中使用了中文等非ASCII字符,可能会出现乱码,是由于编码不一致导致的,因此,需要指定你的文件编码方式:
词法规则与书写格式
词法规则
C/C++的字符集从专业角度说是由ASCII码中的所有可见字符、空格和空字符(ASCII码为0);从一般角度说是英文字符、数字、空格构成。除字符集的中的字符在C/C++语句中均为非法。
标识符是由字母、下划线和数字组成的字符序列,必须以字母或下划线开头,不能是数字,且标识符区分大小写。已经被系统预定义的标识符称为关键字(保留字),用户在定义标识符时不能重复定义,也不能与关键字相重复。
书写格式
C/C++程序书写格式较为灵活,对于单个语句以分号(;)隔开,对于多个语句以花括号隔开({}),以下是相关说明:
- 一行可以写多个语句,一个语句也可以写多行(注意是以词切分)。习惯上一行一个语句,但语句较长时可分多行,语句较短时也可以一行多个语句,尽可能地提高可读性。
- 多个语句构成复合语句(语句块),花括号是复合语句的定界符,分号是单语句的定界符。
- 宏定义和注释不属于语句,因而编译器不会识别注释中的语句,宏定义也不需要写分号。单行注释写在“//”之后,多行注释写在“/*”和“*/”里面
建议
- 对于代码较多的中大型程序或者较为复杂的程序应当尽可能地多使用注释。
- 程序需要顾及可读性和运行效率,代码要求保证可读性的同时尽可能精简。
- 尽可能使用最优的算法,不要用繁杂的方式解决简单的问题。
数据类型的四大基本类型为int(整数型/整型)、float(单精度浮点型)、double(双精度浮点型)、char(字符型)。修饰数据类型的关键字由long、short、unsigned和signed。以下是所有可行的修饰方式和相关数据类型描述:
基本类型 | 占用空间 | 数据表达范围 |
---|---|---|
char | 1字节 | -128到127或0到255 |
unsigned char | 1字节 | 0到255 |
signed char | 1字节 | -128到127 |
int | 2或4字节 | -32,768到32,767或-2,147,483,648到2,147,483,647 |
unsigned int | 2或4字节 | 0到65,535或0到4,294,967,295 |
short | 2字节 | -32,768到32,767 |
unsigned short | 2字节 | 0到65,535 |
long | 4字节 | -2,147,483,648到2,147,483,647 |
unsigned long | 4字节 | 0到4,294,967,295 |
float | 4字节 | 1.2E-38到3.4E+38、6位有效位 |
double | 8字节 | 2.3E-308到1.7E+308、15位有效位 |
long double | 16字节 | 3.4E-4932到1.1E+4932、19位有效位 |
数据类型的五大基本类型为int(整数型/整型)、float(单精度浮点型)、double(双精度浮点型)、char(字符型)、bool(布尔型/逻辑型)。修饰数据类型的关键字由long、short、unsigned和signed。以下是所有可行的修饰方式和相关数据类型描述:
基本类型 | 占用空间 | 数据表达范围 |
---|---|---|
char | 1 个字节 | -128 到 127 或者 0 到 255 |
unsigned char | 1 个字节 | 0 到 255 |
signed char | 1 个字节 | -128 到 127 |
int | 4 个字节 | -2147483648 到 2147483647 |
unsigned int | 4 个字节 | 0 到 4294967295 |
signed int | 4 个字节 | -2147483648 到 2147483647 |
short int | 2 个字节 | -32768 到 32767 |
unsigned short int | 2 个字节 | 0 到 65,535 |
signed short int | 2 个字节 | -32768 到 32767 |
long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
signed long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long int | 8 个字节 | 0 到 18,446,744,073,709,551,615 |
float | 4 个字节 | +/- 3.4e +/- 38 (~7 位数字) |
double | 8 个字节 | +/- 1.7e +/- 308 (~15 位数字) |
long long | 8 个字节 | -9,223,372,036,854,775,807 到 9,223,372,036,854,775,807 |
long double | 16 个字节 | 18-19位数字 |
bool | 1位 | 0和1,也可表示为true和false,本质上是0和1 |
变量与常量
数据类型分为常量和变量,顾名思义,变量是指可以变化的量,而常量是固定不动的值。
对于变量的基本操作,主要有四种:定义、赋值、引用、转换。
在上述例子中,进行了变量a和b的定义。变量使用前必须先定义,其三要素为标识符(名字)、类型和值,定义时需说明前两者,值可以选择说明或者不说明。定义时,变量名表以逗号分隔。下面也是同样的一个例子。
你可能会注意到,我们赋值字符时使用了单引号但赋值数值没有使用,即等号左边是变量而右边是一个常量,常见的常量有以下几种。
常量类型 | 表示方法 |
---|---|
整型 | 十进制直接表示,如154;八进制在数前加0,如022;十六进制在数前加0X或0x,如0x4E、0X6C |
浮点型 | 可以省略小数部分或整数部分其一,默认为0,但不能省略小数点,如0.55、.125、24.、47.20;还可以用指数形式表示,aEb或aeb,等价于a*10^b,例如5E2,0.222e8 |
字符 | 单引号括起来表示,例如's' |
字符串 | 双引号括起来表示,例如"nb","Hello" |
常量类型 | 表示方法 |
---|---|
整型 | 十进制直接表示,如154;八进制在数前加0,如022;十六进制在数前加0X或0x,如0x4E、0X6C |
浮点型 | 可以省略小数部分或整数部分其一,默认为0,但不能省略小数点,如0.55、.125、24.、47.20;还可以用指数形式表示,aEb或aeb,等价于a*10^b,例如5E2,0.222e8 |
字符 | 单引号括起来表示,例如's' |
字符串 | 双引号括起来表示,例如"nb","Hello" |
布尔常量 | true或false |
符号常量 | 如NULL等 |
变量在赋值时,可以使用常量进行赋值,也可以使用变量赋值,通常需要相同类型的量进行赋值,但是也可以使用不同类型的变量进行赋值,此时就需要进行数据类型的转换。显式转换是指人为的强制转换,利用相关的关键字进行转换。隐式转换是指程序识别到被赋值变量和所赋值变量类型不匹配后,自动进行的转换。注意高类型向低类型转换时,是不保值的,会有精度损失。具体请看下面的例子。
C++ | |
---|---|
注意
目前所讲到的基本变量类型全都可以互相转换,但是可能会有数值变化和精度损失,具体原因这里不作解释,和计算机内部数据存储的原理相关。有些数据类型之间不能隐式转换,需要进行人为的强制转换,使用到关键字。
注意变量定义后需要初始化或者赋值,引用未赋值的变量是不合法的,会产生报错,例如:
变量可以进行引用,例如对别的变量赋值、作函数参数、参与运算等等,这些被称为变量的引用。可以使用sizeof关键字获取变量(变量类型)的占用字节。
变量的定义、赋值、引用、转换往往不都是独立发生的,更多的情况下都同时发生。
在你的计算机上运行下面的代码,运行前,对输出的结果进行猜测,然后验证你的猜测。
运算符与表达式
算术运算符
除名称和符号外,运算符有类别、结合性和优先级这三个重要的要素,类别是指运算符的目数,相当于数学中的元数,即其操作数的个数,例如5+6中加号需要两个数相加,其操作数为2,即其为双目。
我们知道,在数学中,有算式的运算顺序,一般都是从左至右运算,运算的符号默认优先和左边的数结合,例如5+-7会认为是5加上-7(但更多会加括号以避免歧义),这里加号优先和左边的数结合,但也有反例,例如2√4,根号优先和右边的数结合,即√4=2,再和左边的数相乘。这就是运算符的结合性。
优先级这个概念就很好理解了,在数学中,乘除的优先级高于加减,在运算时优先于加减,而加减具有相同的优先级。优先级低的先结合,高的后结合。
名称 | 类别 | 运算符 | 结合性 | 优先级 |
---|---|---|---|---|
加 | 双目 | + | 从左至右 | 3 |
减 | 双目 | - | 从左至右 | 3 |
乘 | 双目 | * | 从左至右 | 2 |
除 | 双目 | / | 从左至右 | 2 |
取余 | 双目 | % | 从左至右 | 2 |
取负 | 单目 | - | 从右至左 | 1 |
自增(增1) | 单目 | ++ | 从右至左 | 1 |
自减(减1) | 单目 | -- | 从右至左 | 1 |
下面是算术运算符简单的使用和一些特殊说明。
输入两个数,分别输出其和与差。
输入一个三位数,将其倒序输出。例如输入874,输出478。
尤其要注意自增和自减运算的区别:符号在前先加减,符号在后先返值。运行下面的代码,体会这一差异。
运行前推测下程序的输出结果,并试着运行验证推测是否正确。
注意
上例子中的写法并不推荐!这只是娱乐性的案例,因为C语言在不同标准下,自增和自减有一定的结合性和优先级差异,而且程序代码务必要清晰明了,尽可能不出现有歧义的语句。
关系运算符
名称 | 类别 | 运算符 | 结合性 | 优先级 |
---|---|---|---|---|
大于 | 关系 | > | 从左至右 | 5 |
小于 | 关系 | < | 从左至右 | 5 |
大于等于 | 关系 | >= | 从左至右 | 5 |
小于等于 | 关系 | <= | 从左至右 | 5 |
等于 | 关系 | == | 从左至右 | 6 |
不等于 | 关系 | != | 从左至右 | 6 |
关系运算符,顾名思义,用于判断两个量的关系,这里要注意以下几点:
- 关系运算符的优先级普遍低于算术运算符;
- 关系运算符的结果有且仅有两个,非0即1;
- 关系运算符在参与运算时,不进行隐式类型转换,参与运算的类型需要一致。
赋值等于(=)和比较等于(==)并不一样,不能替换使用,如果误将赋值等于使用为比较等于,会得到意想不到的效果:
运行上面的代码,你会发现:预期的结果应该是不等于,但是a=b却“判断”为了等于,输出了对应结果,这是因为实际上执行了a=b这个赋值操作后,返回了赋值的结果,即返回了3,故得到了对应输出。同样地,可以执行式子a=(b=5)
之后a的值和b的值都是5。
逻辑运算符
名称 | 类别 | 运算符 | 结合性 | 优先级 | 备注 |
---|---|---|---|---|---|
逻辑非 | 单目 | ! | 从右至左 | 1 | 也叫逻辑求反 |
逻辑与 | 双目 | && | 从左至右 | 10 | “一假为假,全真为真” |
逻辑或 | 双目 | || | 从左至右 | 11 | “全假为假,一真为真” |
关于逻辑运算符主要说明以下两点:
- 逻辑运算符预期希望的是布尔类型变量参与运算,如果不是布尔类型变量,则会先隐式转换成布尔类型变量进行运算。其转换方式具体为任何非0值全都转为1(true),0值转换为0(false)。
- 短路规则(堕性求值):由于在逻辑与运算中,“一假为假”,所以多个逻辑与在从左向右结合时,只要出现一个操作数为0,就不再进行后续运算;同样地,由于逻辑或运算中,“一真为真”,所以多个逻辑或在从左向右结合时,只要出现一个操作数为1,就不再进行后续运算。
C++ | |
---|---|
位运算符
名称 | 类别 | 运算符 | 结合性 | 优先级 | 实例 |
---|---|---|---|---|---|
按位非 | 单目 | ~ | 从右至左 | 1 | ~15的值16 |
按位与 | 双目 | & | 从左至右 | 7 | 11&7的值为5 |
按位或 | 双目 | | | 从左至右 | 9 | 11|7的值为15 |
按位异或 | 双目 | ^ | 从左至右 | 8 | 11^7的值为12 |
左移 | 移位 | << | 从左至右 | 4 | 7<<2的值为28 |
右移 | 移位 | >> | 从左至右 | 4 | 7>>2的值为1 |
按位非也叫按位取反。左移位是指左侧为操作数,右侧为位数,将操作数转换为二进制后向左移位数指定个数,多弃少补(0);而右移位是指左侧为操作数,右侧为位数,将操作数转换为二进制后向右移位数指定个数,多弃少补(0)。
下面是位运算律表:
名称 | 运算律 | 备注 |
---|---|---|
按位非 | 0=1,1=0 | - |
按位与 | 0&0=0,0&1=1&0=0,1&1=1 | “一假为假,全真为真” |
按位或 | 0|0=0,0|1=1|0=1,1|1=1 | “全假为假,一真为真” |
按位异或 | 00=0,01=10=1,11=0 | “相同为假,不同为真” |
赋值运算符
除基本赋值运算符外,其它复合赋值运算符都是类似的,实质是简化了其它双目运算。如果设U为一个双目运算符,则aU=b等价于a=aUb,其中a表示变量,b表示表达式。例如a+=b表示a=a+b。
名称 | 类别 | 运算符 | 结合性 | 优先级 |
---|---|---|---|---|
基本赋值运算符 | 双目 | = | 从右至左 | 13 |
复合赋值运算符 | 双目 | +=、-=、*=、/=、%=、&=、^=、|=、<<=、>>= | 从右至左 | 13 |
特殊运算符
除以上基本运算符外,接下来是一些特殊的运算符,部分特殊运算符将在后面的内容讲到。
名称 | 类别 | 运算符 | 结合性 | 优先级 | 实例 | 备注 |
---|---|---|---|---|---|---|
条件 | 三目 | ?: | 从右至左 | 12 | 4<6?5:2的值为5 | a?b:c意为条件a成立时输出b,否则输出c |
逗号 | 逗号 | , | 从左至右 | 14 | 5,7的值为7 | 运算时从左至右,但只输出最后一个值 |
圆括号 | - | () | 从左至右 | 0 | 5*(4+2)的值为30 | 常用于改变优先级 |
下标 | - | [] | 从左至右 | 0 | - | 用于数组,在后文讲到 |
点 | - | . | 从左至右 | 0 | - | 用于结构体、共用体变量,在后文讲到 |
箭头 | - | -> | 从左至右 | 0 | - | 用于结构体、共用体变量,在后文讲到 |
类 | - | :: | 从左至右 | 0 | - | 用于类,在后文讲到 |
取内容 | - | * | 从右至左 | 1 | - | 用于指针,在后文讲到 |
取地址 | - | & | 从右至左 | 1 | - | 用于指针,在后文讲到 |
常量、变量、运算符、函数和输入输出构成了表达式,能够独立成为语句,即表达式语句。
程序控制结构
输出格式控制与转义
定义标识符的语句成为定义语句,除此外还有表达式语句、复合语句和空语句,复合语句是若干语句用花括号括起来组成的一条语句。空语句在语法上需要一条语句,而语义上不需要执行任何操作时使用。
转义符
转义符的出现是为了解决需要对字符进行操作且避免该字符被编译器识别为语句成分时出现错误这一情况。例如某程序需要输出英文双引号("),但是直接进行输出显然会报错,这是因为编译器将需要输出的双引号视为了语句成分,从而出现一个多余的双引号,为了解决这个问题,使用右斜杠进行转义,这时编译器不再将中间需要输出的双引号视为语句词法,而是成为了字符实体。虽然在代码中转义符由多个字符组成,但其实际输出时仅有一个字符。
转义符除了解决上述问题外,还用于扩充字符集,当C/C++不满足于已有的ASCII字符时,用右斜杠加ASCII字符来实现除ASCII字符外字符的功能。上述两个功能中,第一个功能对任意非第二个功能所涉及的字符加右斜杠时,即视为对该字符转义。第二个功能的相关转义符如下表:
转义符 | 意义 |
---|---|
\t | 水平制表符,等价于tab键 |
\v | 垂直制表符 |
\n | 换行符 |
\0 | 终止符,也称为空字符或结束标志字符 |
\ddd | 八进制数 |
\xhh | 十六进制数 |
值得注意的是,C/C++中使用斜杠(也称左斜杠,/)作除法运算符,使用反斜杠(也称右斜杠,\)作转义符。而在计算机系统中,左斜杠常作服务器文件路径(网站链接中多见),右斜杠作本地文件资源管理器路径。
输出格式控制符
在前文中常使用的scanf()和printf()是指标准输入输出,即在程序运行终端上的输入输出,除此外,还有其它输入输出符,在后文说明。下表是常使用的输出格式控制符。
格式控制符 | 说明 |
---|---|
%c | 输出一个单一的字符。 |
%d、%ld | 以十进制、有符号的形式输出short、int、long类型的整数。 |
%u、%lu | 以十进制、无符号的形式输出short、int、long类型的整数。 |
%o、%lo | 以八进制、不带前缀、无符号的形式输出short、int、long类型的整数。 |
%#o、%#lo | 以八进制、带前缀、无符号的形式输出short、int、long类型的整数。 |
%x、%lx | 以十六进制、不带前缀、无符号的形式输出short、int、long类型的整数。如果x小写,输出的十六进制数字也小写;如果X大写,输出的十六进制数字也大写。 |
%#x、%#lx | 以十六进制、带前缀、无符号的形式输出short、int、long类型的整数。如果x小写,输出的十六进制数字和前缀都小写;如果X大写,输出的十六进制数字和前缀都大写。 |
%f、%lf | 以十进制的形式输出float、double类型的小数。 |
%e、%le | 以指数的形式输出float、double类型的小数。如果e小写,输出结果中的e也小写;如果E大写,输出结果中的E也大写。 |
%g、%lg | 以十进制和指数中较短的形式输出float、double类型的小数,并且小数部分的最后不会添加多余的0。如果g小写,当以指数形式输出时e也小写;如果G大写,当以指数形式输出时E也大写。 |
%s | 输出一个字符串。 |
在前文中常使用的cout和cin是指标准输入输出,即在程序运行终端上的输入输出,除此外,还有其它输入输出符,在后文说明。下表是常使用的输出格式控制符,使用后四个控制符前,应当引用头文件:#include <iomanip>
。
格式控制符 | 说明 | 备注 |
---|---|---|
endl | 输出换行符 | - |
dec | 十进制表示 | - |
hex | 十六进制表示 | - |
oct | 八进制表示 | - |
setw() | 数据输出宽度 | - |
setfill() | 填充输出字符 | - |
setprecision() | 输出有效数字位数 | - |
setiosflags() | 输出格式指定 | ios::fixed定点格式,ios::scientific指数格式 |
输入一个十进制数,输出其八进制和十六进制。
选择结构
C++程序控制结构(其实大多数语言的程序结构也是如此)有三种,顺序结构、选择结构(也称为条件结构、判断结构)、循环结构(也称为重复结构)
if……else语句
输入一个整数,判断其是否为奇数还是偶数,奇数则输出1,偶数则输出0。
输入三边a,b,c,判断其是否能构成三角形,能则输出三角形的面积和周长,不能则输出-1.
输入形如ax^2+bx+c=0方程的a、b、c的值,输出该方程的解。
switch……case语句
输入分数,输出其等级,规则为:100分为S,90分以上为A,80分以上为B,70分以上为C,60分以上为D,60分以下为E。
switch语法 | |
---|---|
值得注意的是,switch语句的每一个case子句为入口点,并不能作为上一个入口点的结束,因而需要使用break语句进行分离,一般来说,存在以下等价关系:
if和switch关系 | |
---|---|
输入简单的两数加减乘除的运算式,输出运算结果。例如输入2+4,输出6。
输入年,月,输出对应天数。
循环结构
while和do……while语句
输入一个正整数,将该数倒序输出。
while语法 | |
---|---|
输入若干正数,以输入-1表示输入结束,输出其最大值和最小值。
for语句
输入n,输出从1到n的和。
for语法 | |
---|---|
输入n,输出n!。
累加变量应初始化为0,累积变量应初始化为1;while循环适用于不确定循环次数或未知循环次数的情况,for循环适用于确定循环次数,即已知循环次数的情况。while循环和for循环在一定程度上可以互换。
永续循环又称为死循环,是指永不停息地运行的循环,如while(1)就是一个常用的永续循环,如果在调试程序过程中,结果迟迟没有输出,那么说明进入了死循环,应当检查循环语句。对于有形如永续循环的语句,不一定会进行永续循环,例如:
输出ASCII码表。
输出九九乘法表。
输入n,输出n行菱形图案,即n越大菱形越大。
控制转向语句
语句 | 作用 | 说明 |
---|---|---|
break | 结束(全部)循环/结束case子句 | 仅可用于结束for/while循环或者分离switch的case子句 |
continue | 结束本次循环 | 不执行之后的循环体内容,直接继续下一次循环 |
goto | 跳转到指定标号 | 标号属于标识符,在需跳转的位置以标识符加冒号的形式使用 |
注意
不推荐使用goto,因为其会破坏顺序结构,使得代码的可读性变差。
使用牛顿迭代法解一元方程ax^3+bx^2+cx+d=0在x=xa附近的根,精度为e,输入a,b,c,d,e,xa,输出根。
现有公鸡5元1只,母鸡3元1只,小鸡1元3只,如果用百元买百鸡,有多少种购鸡方案?
C++ | |
---|---|
函数
函数的定义与调用
解决问题的一种方法就可以视为一种函数,一个程序是由众多函数构成的。在一个程序中,解决较小问题的功能模块使用函数实现,对于代码的重用和可读性能够大幅提升,具有很强的应用能力,是大部分程序语言不可或缺的一部分。
使用函数实现乘方运算,输入底数和幂,输出结果。
函数名和变量名类似,也是自行定义的标识符。我们把类型名和函数名称为函数原型,即把函数体去掉后的语句(形参列表可以省略),我们可以用函数原型进行函数的声明。函数声明操作有另一种叫法是提升,把后面的函数提升到了前面,这是从形象角度定义的;而声明是在编译时告诉编译器确实有这么个函数,在前面声明一下,这是从实质角度定义的。
使用函数实现对给定a,b,c三边进行是否该三角形为直角三角形进行判断,在程序中输入三边,输出是否为直角三角形。
函数调用时可以独立成一个语句,不使用其返回值,也可以类似变量那样,出现在表达式中,还可以作为函数的参数。在一个函数中不能定义函数,但在函数中可以调用另一个函数,也可以调用自己,对于有返回值的有参函数而是而言,自己调用自己的情况被称为递归,将在之后讲到。
在函数传递参数时,有三种传递方式:值传递,地址传递和引用传递。之前的案例使用的都是值传递,这种传递方式形参不能影响实参;而地址传递和引用传递形参都能影响实参,将在指针中讲到。
对于函数的参数,可以设置默认值,但由于形参和实参的结合时从右向左进行的,所以设置默认的形参需要置于参数列表的最右方,否则会出错。
计算机程序有时使用循环实现延时,请编写一个延时函数,设置默认值,并在主函数中调用它。
C++ | |
---|---|
除了用户可以自定义函数外,我们还可以使用开发环境预定义的函数,即库函数,C++有丰富的内置库,例如cmath等。具体内容将在后文讲到。
有一种特殊的函数,相当于直接把函数体嵌入到引用处中,称为内置函数,也称内联函数、内嵌函数。一般来说这种函数是为了减少内存开销的提高效率的方法,定义方法和一般函数相同,只需要在首部加上关键字inline就可以了。注意内置函数不能有复杂的程序控制语句且不能是递归函数。
遍历与递归
遍历属于循环中的的概念,可以理解为从遍历结构中逐一提取元素,放在循环变量中,对于所提取的每个元素执行一次语句块。也就是说,遍历循环的次数是给定的,因而for就被称为遍历循环语句,对应地,while被称为条件循环语句。
显然,遍历循环时“公平”的,因为,轮到谁谁就会被先执行,遵循“先来后到”原则。而递归函数遵循“后来先到”原则,当一个函数被调用时,系统会为该函数的局部变量、参数和返回地址分配内存空间,这些信息会被保存在函数调用栈中。当函数执行完成后,相关的内存空间会被释放。在递归函数中,每次调用都会导致新的函数调用栈帧被压入栈中。出栈指的是从栈顶移除一个元素。当执行出栈操作时,栈的大小减小,而被移除的元素就是栈顶的元素。这通常伴随着返回被移除元素的值或执行与该元素相关的操作,也称为弹栈。
请试用递归函数解决汉诺塔问题,输入金片数,输出操作过程。
C | |
---|---|
C++ | |
---|---|
假设有一种粒子能进行分裂,分裂后的两个新粒子平分原粒子具有的能量,但也会耗散一部分,这部分能量占总能量的比率称为耗散率。输入粒子分裂次数,初始能量和耗散率,输出粒子分裂结束后一个粒子所具有的能量。
C | |
---|---|
C++ | |
---|---|
作用域与生存期
变量属性除数据类型属性外,还有空间和时间两大属性。空间属性被称为作用域。一个变量有效的作用范围,即是其作用域。根据作用域进行对变量的分类,可以分为局部变量和全局变量。局部变量是定义在函数或者程序块里的变量,其只在本函数体内或程序块内有效,因而不同函数和程序块内的变量名并不冲突,所以局部变量的作用域是在定义其的函数或程序块内。
全局变量又称外部变量,是定义在函数以外的变量,对整个源代码文件都有效,作用域广,因而每个函数里的代码都可以对全局变量产生影响。但值得注意的是,当外域局部变量和内域局部变量重名时,优先使用内域局部变量,当局部变量和全局变量重名时,优先使用局部变量,若想要使用全局变量,需要使用运算符::指定全局变量即可。
时间属性被称为生存期,即变量在内存中存在的时间,如果未经特殊定义,局部变量的生存期会伴随着函数和程序块的运行结束而结束,而全局变量生存期是整个程序,程序运行结束,全局变量也就被销毁。存储类型分为动态存储方式和静态存储方式,相关说明如下:
存储方式 | 名称 | 声明关键字 | 说明 |
---|---|---|---|
动态 | 自动变量 | auto | 存储在动态数据存储区,可省略,默认值 |
动态 | 寄存器变量 | register | 存储在CPU的通用寄存器中,读写速度更快,但不宜定义过多 |
静态 | 外部变量 | extern | 即全局变量,可以被本文件或着在其它文件中引用,可省略,默认值 |
静态 | 静态变量 | static | 静态局部变量能扩展其生存期到程序结束,不会因为函数或者程序块运行结束而被销毁;静态全局变量能限制其作用域只在本文件内,而不能被其它文件引用 |
运行前推测下程序的输出结果,并试着运行验证推测是否正确。
函数也可以分为两类:内部函数和外部函数。内部函数使用static关键字,只能在本文件内调用;外部函数也可以在其它文件调用,可省略,默认值。
编译预处理
编译预处理是指在编译前,对代码进行的提前处理,用于提高开发效率,便于程序的调试。注意预处理语句不是C++语句,语法也不遵循C++的语法。预处理主要有三种:宏定义、文件包含和条件编译,语法如下:
命名法
命名法是编程中非常重要的一环,它直接影响到代码的可读性、可维护性和可扩展性。一个好的命名法可以使代码更加清晰易懂,降低出错的概率,提高代码的效率和可靠性。本文将介绍常见的命名法,以及如何选择合适的命名。
常用命名法
- 驼峰命名法(Camel Case)是一种常用的命名法,它将单词首字母大写,其余字母小写,由于其看着两边是小写字母,中间是大写字母,像是一个驼峰,故得此名。如:firstName、lastName、emailAddress等。
- 帕斯卡命名法(Pascal Case)也叫大驼峰命名法,它和驼峰命名法类似,但是首字母也要大写,如:FirstName、LastName、EmailAddress等。
- 下划线命名法(Snake Case)是将单词用下划线连接起来,每个单词的字母都小写,如:first_name、last_name、email_address等。
- 匈牙利命名法(Hungarian Notation)是由微软公司提出的一种命名法,它在变量名前加上一个小写字母前缀,表示变量的数据类型,如:strFirstName、iAge、bIsMale等。
命名基本原则
- 名称应该能够清晰地描述变量的含义和作用,避免使用过于简单的变量名,如:a、b、c等,这样不利于代码的阅读和维护。
- 名称应该尽量短小,但是又不能过于简略,应该保证足够描述变量的含义和作用。
- 名称应该避免使用数字和特殊字符,因为它们会让变量名变得难以理解和记忆。
- 名称应该使用正确的命名法,可以使代码更加规范和易读。
数组与指针
数组
对于少量的数据,使用几个变量就可以表示和操作,但对于大量的数据,重复定义大量个变量是非常不现实而繁杂的,因而,引用了数组作为大量数据的存储方法,对数组可以进行访问和操作,下面是对一个数组的一些基础操作。
一维数组
输入n(n < 50),输出斐波那契数列的前n个。
int a[10];cin>>a;
这种写法是错误的,其数组名实质是其首个元素的指针位置。
二维数组
输入n(n < 20),输出杨辉三角的前n行。
二维数组可以看作是特殊的一维数组,而它的成分又是一个数组。二维数组可以推广到多维数组,例如三维、四维等。
输入n(n < 20)个数据,再输入f,从n个数据中寻找和f相等的元素,并返回有几个相等的元素。请使用函数实现。
C | |
---|---|
C++ | |
---|---|
当数组作为函数参数时,只需将数组名写入即可,因为其数组名就是数组第一个元素的地址,其等价于&num[0]
(&是取地址运算,将在后文讲到)。
输入n(n < 20)个数据,再输入f和t,从n个数据中寻找和f相等的元素,全部修改为t。请使用函数实现。
如果你仔细观察,你会发现数组作函数参数传递时,对于形参的操作会影响到实参,而我们之前对于一般变量作函数参数传递时,对于形参的操作却不会影响实参,这种方式称为值传递。而数组名我们说过是第一个元素的地址,即数组作函数参数时的传递方式称为地址传递。值传递具有形实分离的效果,而地址传递具有形实一体的效果。但如何获得一般变量的地址,也实现和数组一样的传递方式呢?将在后面讲到。
指针
如果想要获取一个变量的地址,我们可以使用取地址运算符&,对于变量a,&a就是其地址,但是如果想要把这个地址存储起来,我们可以使用一种专门存储地址的变量,即指针变量。例如对于int a
,可以取其地址int *p=&a
。
输入两个数,交换这两个数并输出。使用函数实现。
如果没有使用指针,函数不会产生应有的效果,只会在其作用域有效。指针很好的解决了这一问题。但是,有关指针的一些问题值得注意:
- 指针不能被直接输入,但可以输出,即对于int *p=&a,cout << p是合法的而cin >> p是非法的。
- 指针必须现指向一个变量,即初始化后才能进行使用,例如int *p;cin >> *p是非法的。
- 指针运算符*没有严格的格式要求,即int * p、int* p和int *p是等价的。
指针使用 | |
---|---|
*称为间址运算符,又成为解引用运算符,在定义时起到指针变量的标识符,在调用时起到指向地址的所对应的量,而指针变量被定义后,如果不带*表示地址,带*表示地址对应的量。
指针可以通过加、减、自增和自减运算进行移动,例如对于int型指针p的p++是指向下移动一个整型变量的位置,若p初值为5000,则移动后的值为5000+1*4=5008。两个指针变量相加减没有实际意义。
指针可以进行比较,低地址端的存储单元小于高地址端的存储单元。
定义两个整型变量,输出其地址较小的变量的地址,使用函数实现。
当指针变量作函数参数和返回值时也是类似的方法,使用*作为指针的标志符,而在调用时不需要再使用标志符,和指针变量的方法类似。
使用指向函数的指针实现指数运算。输入底数和指数,输出结果。
指针与函数 | |
---|---|
字符串
顾名思义,字符串是指一串字符,按照以往的内容,没有专门的类型表示字符串,char只能表示单个字符,不难想到,可以使用字符数组来表示一串字符。C/C++中可以使用这种方法,但为了表示一串字符的结束,使用结束表示字符(也称终止符)“/0”来标识。
输入一串字符(长度不超过100),仅保留其中的大小写字母,去除数字和符号,然后输出操作后的字符串。
C | |
---|---|
C++ | |
---|---|
字符串 | |
---|---|
#include <string>
。
下面是一些字符串的常用函数(需要引用头文件):
名称 | 函数名 | 参数列表 | 返回值 | 作用 |
---|---|---|---|---|
连接函数 | strcat | 目标字符串,*源字符串缓冲空间值*,源字符串 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将源字符串连接到目标字符串之后,连接后源字符串的内容不变 |
直接复制函数 | strcpy | 目标字符串,*源字符串缓冲空间值*,源字符串 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将源字符串复制到目标字符串,连接后源字符串的内容不变,目标字符串被覆盖 |
指定复制函数 | strncpy | 目标字符串,目标字符串长度,源字符串,欲拷贝字符数 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将指定字符数的源字符串复制到目标字符串,连接后源字符串的内容不变,目标字符串被覆盖 |
长度函数 | strlen | 字符串 | 字符串长度值 | 求字符串实际长度(不包括\0) |
比较函数 | strcmp | 字符串1,字符串2 | 0、正整数、负整数 | 字符串1和字符串2自左向右逐个字符比较ASCII码,直至出现不同字符或者遇到\0为止。如果字符都相等,则返回0,若字符串1不相等的首个字符小于与字符串2,则返回负数,否则返回正数。实质是返回两字符串不相等的首个字符的ASCII码差值 |
转大写/小写字母函数 | strupr/strlwr | 欲要转换的字符串 | - | 将字符串的小写字母转大写/大写字母转小写 |
查找函数 | strstr | 字符串1,字符串2 | NULL、字符串2的位置 | 在字符串1里寻找字符串2,若找到,返回字符串2首次出现的位置,否则返回NULL |
名称 | 函数名 | 参数列表 | 返回值 | 作用 |
---|---|---|---|---|
连接函数 | strcat_s | 目标字符串,*源字符串缓冲空间值*,源字符串 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将源字符串连接到目标字符串之后,连接后源字符串的内容不变 |
直接复制函数 | strcpy_s | 目标字符串,*源字符串缓冲空间值*,源字符串 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将源字符串复制到目标字符串,连接后源字符串的内容不变,目标字符串被覆盖 |
指定复制函数 | strncpy_s | 目标字符串,目标字符串长度,源字符串,欲拷贝字符数 | 0:成功、EINVAL:字符串未初始化、ERANGE:越界 | 将指定字符数的源字符串复制到目标字符串,连接后源字符串的内容不变,目标字符串被覆盖 |
长度函数 | strlen | 字符串 | 字符串长度值 | 求字符串实际长度(不包括\0) |
比较函数 | strcmp | 字符串1,字符串2 | 0、正整数、负整数 | 字符串1和字符串2自左向右逐个字符比较ASCII码,直至出现不同字符或者遇到\0为止。如果字符都相等,则返回0,若字符串1不相等的首个字符小于与字符串2,则返回负数,否则返回正数。实质是返回两字符串不相等的首个字符的ASCII码差值 |
转大写/小写字母函数 | strupr/strlwr | 欲要转换的字符串 | - | 将字符串的小写字母转大写/大写字母转小写 |
查找函数 | strstr | 字符串1,字符串2 | NULL、字符串2的位置 | 在字符串1里寻找字符串2,若找到,返回字符串2首次出现的位置,否则返回NULL |
值得注意的是,字符串不能进行相等“==”或不相等“!=”比较,而是使用比较函数进行比较。
输入一串字符(例如一篇文章),输出大写字母、小写字母、数字、符号及空格的数量。
指针与数组
一般而言,数组中元素存储的地址是连续的,我们可以根据这一规律使用指针访问数组内的不同元素,首先我们研究一维数组如何使用指针表示,已有数组a[n],且*p=a或(*p=&a[0]),则对于数组a的元素,可有以下表示方法: |下标法|指针法| |-|-| |a[0]、p[0]|*a、*p| |a[i]、p[i]|*(a+i)、*(p+i)|
输入n个整型数据(n < 100),将这n个数据倒序输出。
接下来我们研究二维数组使用指针指定元素的方法。二维数组广义上讲可以认为就是一维数组,在此之前,简要说明指针法和下标法的对照表示:
下标法 | 下标-指针法 | 指针法 | |
---|---|---|---|
首地址 | - | &a[0] | a、*a |
首值 | a[0][0] | *a[0] | **a |
i行首地址 | - | a[i]、&a[i][0]、&a[i] | a+i、*(a+i) |
i行首值 | a[i][0] | *a[i] | **(a+i) |
i行j列首地址 | - | a[i]+j、&a[i][j] | *(a+i)+j |
i行j列首值 | a[i][j] | *(a[i]+j)、(*(a+i))[j] | *(*(a+i)+j) |
下面的案例会使你对用指针表示数组有新的认识:
先思考下面程序的可行性,再试着上机运行以验证你的猜想。
把代码复制进编辑器后,点击运行,没有报错,然后输入数据调试,结束运行的一刻,出现了调试错误窗口(这是可能的情况之一)。但可以看到控制台上,能进行正确的输出。这个程序可以说是擅自创建了一个一维数组,但这种直接对内存操作的方式属于越权访问,是禁止的,但是我们可以对系统进行申请空间,而不是擅自使用,申请空间使用到关键字new,我们可以使用一个指针变量将申请的空间的首地址存储起来,当我们使用完后,再进行释放,把空间归还给系统。这样一来,就解决了不能用变量指定数组元素个数的尴尬情况:
动态申请空间 | |
---|---|
输入若干数据,以0作为结束,再输出这些数据。
我们通过之前的内容了解到,数组名的实质是指针,那么,数组可以存储指针变量吗?显然是可以的,这就需要说到指针数组,其定义方法和指针变量类似,在其标识符前加一个*号即可。除此之外,还有多级指针,即指针的指针,一个二级指针的定义如下:
多维指针 | |
---|---|
数组除二维数组还有三维、四维……数组、指针除二级指针还有三级、四级……指针。
在C++中,除了可以使用变量名直接访问变量、使用地址访问变量外,还可以使用引用。引用就是给变量起一个别名,其效用和变量本名等价。引用的方法如下:
引用 | |
---|---|
引用是没有次数上限的,也就是说,一个变量可以有很多个别名。引用可以作函数参数和返回值。
自定义数据类型
除了之前讲到的变量类型外,本节将讲解其它一些特殊的变量类型,包括结构体类型、共用体类型、枚举类型这三类可以自定义的数据类型,在描述一些复杂的数据对象时,这些类型起到重要作用。例如结构体类型时不同类型数据的组合,且子数据直接彼此存在联系。
结构体
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构。结构体和其他类型基础数据类型一样,例如int类型,char类型只不过结构体可以做成你想要的数据类型。以方便日后的使用。在实际问题中有时候我们需要几种数据类型一起来修饰某个变量。这些数据类型都不同但是他们又是表示一个整体,要存在联系,那么我们就需要一个新的数据类型。结构体就将不同类型的数据存放在一起,作为一个整体进行处理。
有10个学生,每个学生的数据包括学号、姓名、三门课的成绩,从键盘输入10个学生的数据,要求打印出三门课的平均分,以及最高分和最低分的学生的相关信息。
结构体类型:
结构体 | |
---|---|
共用体
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
有10个学生,每个学生的数据包括学号、姓名、课程(有的学生有三门课,有的学生有四门课)的成绩,从键盘输入10个学生的数据,要求打印出课程的平均分,并按课程数分类将学生姓名打印出来)。
共用体类型:
共用体 | |
---|---|
共用体 | |
---|---|
枚举和自定义类型
除结构体和共用体外,还有枚举类型enum和自定义类型typedef。
枚举类型:
枚举类型的本质是枚举表中第一项起分别是0,1,2...的列表,使用枚举符进行赋值,使得其值为对应的项号,默认情况下是这样的,也可以指定其项号,对于不指定的,自动进行顺延。例如enum sex{female=2,male,none},male的项号为3,none的项号为4。
自定义类型其实就是给数据类型起一个别的名字而已,既没有创造一个新的变量类型,也没有影响原来的变量类型,原变量标识符仍然是可用的。
C输入输出流
C的输入输出是通过流的方式进行的。C语言中的输入输出主要通过标准输入输出库(stdio.h)来实现,包括键盘输入、屏幕输出以及文件输入输出。C++输入输出见“C++面向对象”。
标准输入输出流
在C语言中,标准输入输出流主要通过以下函数来实现:
scanf
: 从标准输入(键盘)读取输入。printf
: 将输出内容发送到标准输出(屏幕)。fprintf
: 将输出内容发送到文件流。fscanf
: 从文件流读取输入。
C | |
---|---|
文件操作与文件流
C语言中文件的操作相对复杂一些,需要手动打开和关闭文件,并使用不同的函数进行读写操作。
打开/关闭文件
C | |
---|---|
文本文件读写
C | |
---|---|
二进制文件读写
其他常用操作
C | |
---|---|