总结C语言中经常忘记、易混淆的知识,后续遇到其他问题再补充。若有错误,欢迎指正。
数据类型
C语言规定了各种类型的最小大小,但没有明确规定各种类型的具体大小。
- 基本类型 —— short、int、long、float、double、char
- 构造类型 —— [ ]、struct、union、enum
- 指针类型 —— *
- 空类型 —— void
变量
程序运行期间可以发生改变的量,本质是一块内存空间。
声明
- 不开辟内存空间的声明:
extern int a;
- 开辟内存空间的声明(定义):
int a;
定义
- 开辟内存空间并赋值:
int a = 0;
区别
- 声明可以有多次,定义只能有一次
- 声明可能不需要开辟空间,定义一定开辟内存空间
- 在函数体内不能初始化被extern标记的变量
extern int a; //没有分配内存空间
int b; //分配内存空间0x0078FCDC
常量
常量也称为字面量,是固定值,在程序运行期间不改变,可以是任何基本数据类型。
整型常量
-
二进制不能表示整型常量,只有八进制、十进制、十六进制3种表示方式。
-
前缀指定用何种进制解释常量
-
后缀限定常量的类型(不区分大小写)
//前缀 int a = 12; //默认十进制a=12 int b = 012; //八进制b=10 int c = 0x12;//十六进制c=18 //后缀 整型常量默认 int 1234; //int 1234u; //u -> unsigned 1234l; //l -> long 1234lu; //lu、ul -> unsigned long 1234ll; //ll -> long long 1234llu;//llu -> unsigned long long
浮点型常量
只能使用十进制,有两种形式:
-
小数形式:-1.23
-
指数形式:1.23e+2 = 1.23*10^2
-
后缀限定浮点常量的类型
//浮点常量默认 double 1234.0; //浮点常量默认 double 1234.0f;//f 指定为 float 1234.0l;//l 指定为 long double
#define & const
-
#define定义的常量不占内存,编译后就不存在,效果相当于直接替换。
#include <stdio.h> #define PI 3 int main() { printf("%d\n", PI * 2); // <=> 3 * 2 = 6 return 0; }
-
const
定义的常量的值不能被直接修改,不能被赋值,但可以使用指针修改const常量的值。const int a = 10; //a = 6; //表达式必须是可以修改的左值,无法通过编译
-
#define & const 区别
#define const 起作用阶段 预编译时作用 编译、运行时作用 作用方式 直接替换 类型检查 存储空间 符号常量的名字不分配存储空间 占用存储空间,变量值不改变 调试 无法调试 可调试
格式化输入输出
printf()
格式化输出函数,向 stdout
按规定格式输出信息。在输出时:
- 普通字符:按原样复制到标准输出
- 转换说明:按转换说明符控制 printf 要输出数据的类型、宽度、精度等。
- 返回值:要显示的字符串的长度
转换说明组成:%[标记][最小宽度].[精度][长度修饰符][转换说明]
标记
[长度修饰符]
常见的长度修饰符有:h(short)、l(long)、ll(long long)、z(size_t), 如果长度修饰符与转换说明不匹配则会引起未定义的行为。[转换说明]
#include <stdio.h>
int main() {
int x = 6;
float f = 11.234;
printf("x = |%d|\n", x);
printf("x = |%-4d|\n", x); //向左对齐 |6 |
printf("x = |%4d|\n", x); //向右对齐 | 6|
printf("x = |%04d|\n", x); //使用0补齐|0006|
printf("f = |%f|\n", f); // |11.234000|
printf("f = |%10f|\n", f); //宽10位,默认小数点后6位 | 11.234000|
printf("f = |%-10.2f|\n", f);//左对齐,宽10位,小数点后2位|11.23 |
printf("f = |%10.0f|\n", f);//右对齐,无小数 | 11|
//printf返回值 => 显示字符的个数
int pret = printf("Hello world.\n");
printf("printf返回值:%d\n", pret);
return 0;
}
scanf()
格式输入函数,从进程空间的标准输入缓冲区 stdin
中读取字符与转换说明进行匹配,该过程中若
- 匹配成功:将读取的字符、数字从缓冲区删掉,并继续向后匹配直到格式串匹配完,返回匹配成功的个数。
- 匹配失败、到达文件末尾、发生读错误:立即返回**
EOF
**,它在 stdio.h 中的定义是#define EOF (-1)
。
格式组成:%[*][域宽][长度修饰符][转换说明]
-
读取基本类型的变量:
&变量名
;读取字符串、指针变量不使用&。C语言中的字符串由**
字符数组+\0
**组成,而数组名、指针变量名本身就是地址,因此使用scanf()函数时,不用在它们前面加上&
。 -
[*]
: 表示忽略读入的数据#include <stdio.h> int main() { // * 忽略读入的数据 int a[3] = { 0 }; //初始全0 scanf("%d %*d %d", &a[0], &a[1], &a[2]); //%*d被忽略 若输入10 9 8,9会被忽略 printf("a[0]=%d,a[1]=%d,a[2]=%d", a[0], a[1], a[2]);//a[0]=10,a[1]=8,a[2]=0 return 0; }
-
[域宽]
:非零十进制整数,指定宽度(最多读取的字符数),分隔字符串。
eg:输入一个字符串,按长度为8拆分每个输入字符串(每个字符串长度小于等于100)(牛客网:华为机试)// 输入:abc // 输出:abc00000 #include <stdio.h> #include <string.h> int main(){ char str[100]; while(scanf("%8s", str) != EOF) { int len = 8 - strlen(str); printf("%s", str); for(int i = 0; i < len; i++) printf("0"); printf("\n"); } }
-
[长度修饰符]:同printf()。
-
[转换说明]
匹配的规则(同
printf()
的转换说明一样),按规则将数据转换为对应的二进制数据。发生匹配时,普通字符一对一精确匹配,空白字符(空格、换行符、制表符等)可以匹配任意多个。其中-
%s
:读取字符串时遇到\0、\n
即停止。 -
%c
:读取空白字符(空格、\n、制表符等)在内的所有字符,所以混合输入时,%c
前加一个空格。匹配时空格
负责匹配读入的空白字符,而%c
就可以匹配其他字符。eg:%s
和%c
同时使用,在%c
前加空格,让空格
去匹配字符串最后的\0
**,%c
则匹配其他普通字符。#include <stdio.h> int main(){ char string[20], t; scanf("%s %c", string, &t); //输入:test hh printf("string:%s t:%c", string, t); //string:test t:h return 0; }
-
-
其他
scanf()
是行缓冲,当行缓冲区空时阻塞;非空时一直从行缓冲区中读取并匹配字符。#include <stdio.h> int main(){ int i; char c; scanf("%d", &i); printf("i=%d\n", i); scanf("%d", &c); printf("c=%d\n", c); //按%d从缓冲区匹配,不会匹配换行符 scanf("%c", &c); //上一个printf取走一个整型数后,还有\n在缓冲区中(非空),%c会将\n匹配给变量c printf("c=%c\n", c); //变量c是\n,加上本行\n共输出两个换行符 return 0; }
运行结果:
存储类别
C语言中,每一个变量和函数都有两个属性:数据类型、数据存储类别。存储类别定义了变量、函数的范围、生命周期,有以下几类:
-
auto:所有局部变量默认的存储类别,只能用在函数内,修饰局部变量。没有使用static声明的、动态分配存储空间的变量都属于该类。
-
static:把局部变量分配在静态存储区,限制全局变量、函数作用域为该文件。
-
register:将变量存储在寄存器中。
- 当所有寄存器被占用时不能被分配到寄存器中
- 不能对分配到寄存器中的变量使用指针、&(因为寄存器没有内存地址)。
-
extern:两个或多个文件共享全局变量或函数时使用,拓展变量的作用域。
static & auto
static
在静态存储区分配内存单元;auto
在动态存储区分配内存单元static
编译时赋初值,且只赋一次初值;auto
在函数调用时赋值,调用一次赋一次值static
定义时若未赋值,编译时赋值0、\0
;auto
若未赋值,值不确定
#include <stdio.h>
int main() {
int a; //VS2022:cc cc cc cc
static int b1; //默认赋初值0
static int b2=6; //6
register int c=0; //0
printf("%d %d %d", b1, b2, c);//0 6 0
return 0;
}
运算
重点关注运算符的两个属性:优先级、结合性(从左向右结合、从右向左结合)
优先级
简单记忆:算数运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符 。详细优先级见C语言运算符优先级图。
sizeof
-
sizeof()
是一个运算符,而非函数,在编译阶段就获得结果。 -
计算某一类型占内存空间的大小(以字节为单位),返回size_t类型的值(无符号整型,使用“zd”、“%u”等转换说明)
模运算 %
-
只能对整数取模,使用printf打印
%
时转换说明是%%
。 -
若对负数取模,取模的结果符号同第一个运算对象的符号,eg :
-11 % 5 = -1
、11 % -5 = 1
#include <stdio.h> int main() { printf("%d %% %d = %d\n", 4, 3, 4%3); //4 % 3 = 1 printf("%d %% %d = %d\n", 4, -3, 4%-3); //4 % -3 = 1 printf("%d %% %d = %d\n", -4, 3, -4%3); //-4 % 3 = -1 printf("%d %% %d = %d\n", -4, -3, -4%-3);//-4 % -3 = -1 return 0; }
-
判断整数奇偶性(%、位运算&)
#include <stdio.h> #include <stdbool.h> bool is_odd(int x) { return x % 1 != 0; } int main() { // % 判断奇偶性 if (is_odd(6)) printf("odd\n"); else printf("even\n"); return 0; }
赋值运算
-
赋值过程可能发生隐式类型转换
-
=
从右向左结合,eg:i = j = k = 2;
等价于i = ( j = (k = 2));
#include <stdio.h> int main() { float f; int i; f = i = 6.66f; printf("i=%d f=%f", i, f);//i=6 f=6.000000 return 0; }
自增(减)运算
-
++
、--
只能作用于变量,包括:char、int、float。 -
i++
:表达式的值为 i;++i
:表达式的值为 i+1,都会产生副作用 —— i 自增#include <stdio.h> int main() { //后++(后-- 一样) int i = 6; printf("i=%d\n", i++);//i++ 的值为i => 6 printf("i=%d\n", i); //i++ 的副作用 => i自增,i=7 //前-- (前++ 一样) printf("i=%d\n", --i);//--i的值为i-1,产生副作用 = > i自减,i=6 printf("i=%d\n", i); return 0; }
-
混合运算时,++i 和 i++ 自增顺序不同:
++i
先自增再赋值;i++
先赋值再自增。eg:q = 2 * ++i // i 先自增再乘以2,再赋值给q q = 2 * i++ // i 先乘以2再自增,再赋值给q
-
错误用法
i=i++;
j=(++i)+(i++);
a[i]=b[i++];
类型转换
类型级别从低到高:int、unsigned int、long、unsigned long、long long、unsigned long long、float、double、long double
- char、short => int => long => long long => float => double
- signed => unsigned
- 作为函数参数传递时,char 和 short 转为 int(整数提升),float 转为 double
- 强制转换、隐式转换,都只是为了本次运算的需要而对变量的数据长度进行的临时性转换,而不改变数据声明时对该变量定义的类型。
隐式类型转换
-
编译器自动转换
-
signed
和unsigned
不要混合运算,会出现意料之外的结果(编译器会自动将signed转换为unsigned)。#include <stdio.h> int main() { unsigned int a = 100; int b = -1; if (a > b) //b发生类型转换,由signed => unsigned -1补码1111 1111 (255) printf("a > b"); else printf("a < b"); return 0; }
强制类型转换
-
强制类型转换只转换紧跟的一个值,要使用
()
才能转换表达式的值。#include <stdio.h> int main(){ int a = 5, b = 2; float c1 = a / b; //2.00000000 float c2 = (float)a / b + b; //4.50000000 float c3 = (float)(a / b + b); //4.00000000 return 0; }
-
计算浮点数小数部分
double d = 3.14, fraction; fraction = d - (int)d;
-
避免溢出
#include <stdio.h> int main() { long long a = 10 * 10 * 10; long long b = 1000 * 1000 * 1000 * 1000;//1000默认是int型,则1000*1000*1000*1000也是int型,但是发生了溢出 //赋值给b的是一个溢出后的int值 //避免溢出 long long c = (long long)1000 * 1000 * 1000 * 1000;//强制类型转换后不会发生溢出 printf("b/a=%lld\n", b/a); //-727379 b溢出,计算错误 printf("c/a=%lld\n", c/a); //1000000000 强制转换后b不再溢出 return 0; }
整数运算
C语言中两个整数相除或取余其结果依旧为整数,当两个整型数相除结果为小数时,先强制类型转换再运算才可以输出小数。eg:
#include <stdio.h>
int main(){
int i = 5;
float j = i / 2; //2.000000
float k = float(i) / 2; //2.500000
printf("%f %f\n", j, k);
return 0;
}
浮点数比较
通过两个数相减结果是否为零判断两个浮点数是否相等,不能使用 ==
。
浮点数按照IEEE754标准存储,由于浮点数的精度问题,有些浮点数只是一个近似值(eg: 23.45 的值为 23.4500008)因此要判断两个浮点数是否相等,需要将两个浮点数作差,如果差值在某个范围内则相等,否则不相等,这个范围要根据浮点数的精度判断。如下实例中由IEEE754标准float精度为7位,234.56已使用3位有效精度,所以和0.0001、-0.0001比较,只要在该范围内,即是相等。
#include <stdio.h>
int main(){
float f = 234.56;
//错误方法,输出f is not equal to 234.56
if (f == 234.56)
printf("f is equal to 234.56\n");
else
printf("f is not equal to 234.56\n");
//正确方法,输出f is equal to 234.56
if (f - 234.56 > -0.0001 && f - 234.56 < 0.0001)
printf("f is equal to 234.56\n");
else
printf("f is not equal to 234.56\n");
return 0;
}
短路运算
-
可以使用短路运算替代
if
-
expression1 && expression2 : 当expression1的值为0时与任何表达式相与结果都为0,这时不会再执行expression2。
#include <stdio.h> int main(){ int i = 0; i && printf("Error!"); //不会打印Error! <=>if(i) printf("Error!"); return 0; }
-
expression1 || expression2 运算时 :当expression1的值为1时与任何表达式相与结果都为1,这时不会再执行expression2’
#include <stdio.h> int main(){ int i = 1; i || printf("Error!"); //不会打印Error! return0; }
-
当存在短路运算时,优先级最高的
()
也会被短路#include <stdio.h> int main(){ int i = 0, j = 4; i && (i = j); //()被短路,不起作用 printf("%d\n", i); //0 i=1 || (i = j); //()被短路,不起作用 printf("%d\n", i); //1 return 0; }
位运算
-
左移 <<
-
左移相当于×2。由于CPU中没有加法器,有移位器,所以移位运算比乘法运算效率高。
-
有符号正数左移可能变为负数,有符号负数左移可能变为正数。为了代码的移植性,最好不要对有符号数进行移位运算。
-
**使用
<<
开辟空间 **,eg : 使用malloc(1<<30)
(1G = 230)申请1G内存空间。#include <stdio.h> int main() { short i1 = 0x7385, i2 = 0x8051; short j1 = i1 << 1, j2 = i2 << 1; //i1 = 7385 (29573) i1<<1 = -6390 printf("i1 = %X \(%d\) i1<<1 = %d\n", i1, i1, j1); //i2 = FFFF8051 (-32687) i2<<1 = 162 printf("i2 = %X \(%d\) i2<<1 = %d\n", i2, i2, j2); return 0; }
-
-
右移 >>
#include <stdio.h> int main(){ int i1 = 7, i2 = -7; printf("%d %d", i1>>1, i2>>1); // 3 -4 return 0; }
-
移位运算不改变原来变量的值 ,但移位赋值可以改变变量的值。
#include <stdio.h> int main(){ int i = 8; int j = 6; printf("i >> 1 = %d, i = %d\n", i>>1, i); //i >> 1 = 4, i = 8 printf("j <<=2 = %d, j = %d\n", j <<= 2, j);//j <<=2 = 24,j = 24 return 0; }
-
&
判断奇偶性bool is_even(int x){ return x & 1 == 0; }
-
^
性质:a ^ 0 = a
、a ^ a = 0
、a ^ b = b ^ a
、(a^b)^c = a^(b^c)
-
判断整数是否是2的次幂(2^n 的二进制表示只有一个1):
//1.左移赋值判断 bool isPowerOf2(unsigned int x){ unsigned int i = 1; while (i<x){ i <<= 1; } return i == x; } //2.按位异或 bool isPowerOf2(unsigned int n){ return (n ^ n) - 1 == 0; }
-
逗号 ,
连接多个表达式,有括号时值为最后一个表达式的值,没有括号时值为第一个表达式值。
#include <stdio.h>
int main() {
int a=0,b,s=2,d=3;
b=a,d+2; //b=0
a=12+(s+2,d+4); //a=19
return 0;
}
控制流
循环边界
i ∈ [ 0 , n ) i\in \left [ 0,n \right ) i∈[0,n) 与 i ∈ [ 1 , n ] i \in \left [ 1,n \right ] i∈[1,n] 循环次数相同,循环结束时的 i 不一样:
for(int i = 0; i < n; i++)
:循环 n 次,循环结束时 i == n;for(int i = 1; i <=n; i++)
:循环 n 次,循环结束时 i == n+1;for(int i = m; i < n; i++)
:循环 n - m 次,循环结束时 i == n ;for(int i = m; i <=n; i++)
:循环 m - n + 1 次,循环结束时 i == n+1 。
break
-
跳出
switch
、while
、for
循环,当它们嵌套时,只能跳出包含break
的最内层嵌套。 -
防止case穿透
continue
-
跳转到循环体末尾,本次循环的剩余部分不再执行,准备开始下一次循环(并没有跳出当前循环)。
-
用作占位符。
while(getchar()!='\n'); continue;
goto
-
在同一个函数内跳转到相应标签语句
-
在
case
内跳出while
循环、多层while
嵌套跳出循环时只能使用goto
while(1){ switch(){ case 1: ; ...... } } while(1){ while(...){ while(...) ... } }
-
错误处理
#include <stdio.h> int main(){ int count = 16; while (1){ for (int i = 0; ; i++){ if(i>count) goto error_handle; } } error_handle: printf("error_handle!"); return 0; }
switch-case
- 表达式值必须是整数类型
case
后必须是整数类型的常量表达式,不能有重复的标签- 效率、可读性比级联式
if...else
高,但是不如它常用。
case穿透
当某个 case
语句中没有break时,程序会一直执行直到遇到 break。
#include <stdio.h>
int main() {
int score;
scanf("%d", &score);
switch (score/10){ //表达式必须是整型、枚举类型
case 10: //90-100视为优秀,当score=100(case=10)时,由于没有break;会向下执行case9的语句,遇到break时跳出分支
case 9:
printf("优秀\n");
break;
case 8:
case 7:
printf("良好\n");
break;
case 6:
printf("及格\n");
break;
default:
printf("不及格\n");
break;
}
return 0;
}
数组
-
相同数据类型有序、连续存储在栈空间,初始化时就确定了大小。
-
数组下标从0开始,若下标从1开始,则每一次寻址都会多一次减法运算:
-
下标从0开始:a[i] _address = base_address + i * type_size ;
-
下标从1开始:a[i] _address = base_address + ( i - 1) * type_size
舍弃a[0],从第二个元素a[1]开始存数组元素也是可以的,但是会浪费内存。在早期的时候计算机的计算、内存资源比较宝贵,故下标从0开始。
-
一维数组
-
初始化
int a[5] = {4, 5, 6, 7, 8}; //初始化所有元素 int c[5] = {0}; //所有元素赋初值0 int b[5] = {1,2,3}; //部分元素赋值,其余元素为0.数组b所有元素:1,2,3,0,0 int e[5]; e[0]=2; e[2]=4; //部分元素赋值,其余元素脏数据、随机数据 int d[ ] = {1,2,3}; //编译器自动匹配数组大小、元素个数 int f[ ] = {0} //!!!!只有一个元素,值为0,错误用法
不允许有以下情况:
int a[10]; a[10] = {0,1,2,3,4,5,6,7,8,9};
原因:数组元素大小确定之后,a[x]代表访问某个元素。
a[10] = {1,2,3}
就是给第11个元素赋值1,2,3这显然是不可行的。除此之外,a[10]访问第11个元素会造成数组访问越界。 -
数组名a代表首元素a[0]的地址,一维数组名(一维数组首地址) ⇔ \Leftrightarrow ⇔ 一维数组元素首地址。eg:int a[3];a == &a[0]
#include <stdio.h> int main() { int a[3]; printf("a=%x, &a[0]=%x\n", a, &a[0]); //b3fd84 b3fd84 return 0; }
-
sizeof(array)/sizeof(array[0])
:计算数组元素个数。当**一维数组做函数形参时,不能这样用!**#include <stdio.h> //宏函数求数组大小 #define SIZE(a) (sizeof(a)/sizeof(a[0])) int main(){ int arr[10]={0}; printf("SIZE(arr)=%d", SIZE(arr)); return 0; }
二维数组
-
C语言中二维数组以行优先方式存储,存满一行再往下存。
-
初始化:二维数组的一维大小可以省略,不能省略第二维的大小。eg:
int array[][10]
、int array[10][10]
。int a[3][3] = { {1,2,3},{5,6,7},{8,9,0} }; int b[3][3] = { {1},{2,3},{4,5,6} }; int c[3][3] = { 1,2,3,4,5,6 }; //编译器按行优先自动分配,其余元素为0 int d[ ][3] = { 1,2,4,6,8 }; //编译器按行优先自动分配,其余元素不确定(随机值、脏数据) int e[3][3] = {0} //全0二维数组
-
二维数组的数组名(二维数组首地址) ⇔ \Leftrightarrow ⇔ 二维数组的首元素地址 ⇔ \Leftrightarrow ⇔ 数组的首行地址。eg:int a[3][3],a == &a[0][0] == a[0]
#include <stdio.h> int main() { int a[3][3]; printf("a=%x, a[0]=%x, &a[0][0]=%x\n", a, a[0], &a[0][0]);//a=fbf854, a[0]=fbf854, &a[0][0]=fbf854 return 0; }
-
求二维数组行数、列数
#include <stdio.h> #define LINE(matrix) (sizeof(matrix)/sizeof(matrix[0])) //二维数组行数 #define ROW(matrix) (sizeof(matrix[0])/sizeof(matrix[0][0]))//二维数组列数 int main() { int a[][3] = {{0,1,2},{3,4,5},{6,7,8}}; printf("%d line, %d row\n", LINE(a), ROW(a)); return 0; }
常量数组
-
数组元素不能被改变,存放静态数据。
const int arr[4] = { 1,2,3 }; //arr[2] = 6; //表达式必须是可修改的左值
访问越界
数组越界后会把数据放到未知区域,该区域原来的值被覆盖。如果该区域是系统的某个重要区域,可能因为数据被修改而造成系统崩溃。编译器并不会检查程序对数组下标的引用是否在数组的合法范围内,好的做法是:
- 下标值是通过已知正确的值计算得来的,可以不检查。
- 由用户输入的数据产生的下标值,在使用之前必须进行检查,确保在有效范围内。
#include <stdio.h>
void print(int b[], int len){
int i = 0;
for (i = 0; i < len; i++)
printf("a[%d]:%d \n", i, b[i]);
}
int main(){
int j = 10;
int a[5] = { 1,2,3,4,5 };
a[5] = 8; //此时数组已经越界
a[6] = 7;
a[7] = 4;
print(a,5); //a[5]所有元素:1 2 3 4 5
printf("j=%d", j); //j=4 j本应该是10,但由于数组越界导致j=4
return 0;
}
结果:
上述程序在 a[5]=8
处已经发生数组越界。因为微软的编译器设计了不同变量之间有8个字节的保护空间(Mac和Linux没有),为了看数组越界会修改变量值的效果,17、18行继续赋值。由运行结果可以看到,数组越界导致原来的 j 被越界的 a[7] 覆盖。
字符串
-
字符数组:必须使字符数组大小比字符个数多,用
\0
作为结束的标志。char a[10] = {'h','e','l','l','o','\0'};
-
字符串
char a[8]="Hello"; char b[]="Hello!"; //编译器自动添加‘\0’
-
字符串常量:用
""
括起来的字符序列,eg:"hello world"
,编译器会把相邻(以空白字符分割)的字符串常量拼接成一个字符串常量。printf("hello" " world" "!"); //hello world!
-
字符串变量:C语言没有专门的字符串类型,使用字符数组存储字符串。
-
读写字符串
-
读写单个字符:
getchar()
、putchar()
,效率比printf、scanf高。 -
读写一个字符串:
gets()
、puts()
getchar()
int getchar(void);
从 stdin
读取下一个字符(包括空格、换行符、制表符),等价于 getc (stdin)
。
-
跳过一行多余的字符
while( getchar() != '\n' ){ continue; }
putchar()
int putchar( int ch );
向 stdout
写入一个字符,字符 ch
在写入前被转换为 unsigned char,等价于 putc(ch, stdout)
。
puts()
int puts( const char *str );
-
将一个字符串写到
stdout
,输出后自动添加\n
。 -
返回值:成功——非负值;失败—— -1。
gets()
char *gets( char *str );
-
用于从
stdin
读取字符串(包含空格),直到出现\n、EOF
,在读取最后一个字符后立即添加\0
,并输出一个换行符。#include <stdio.h> int main(){ char c[20]; //字符数组的数组名存的是字符数组的起始地址,类型是字符指针(char[20]*) gets(c); //一次读取一行使用gets() puts(c); //等价于printf("%c\n",c); return 0; }
-
scanf()
结合正则表达式使用,也可以实现**gets()
**的效果。如:使用%[^\n]s
获取带有空格的字符串。#include <stdio.h> int main() { char str[10]; scanf("%[^\n]s", str); printf("%s", str); return 0; }
由于 scanf()、gets()
都不知道要获取的字符串的大小,可能会发生越界(缓冲区溢出),因此 scanf()
、gets()
都是不安全的。
字符串数组
-
使用二维数组表示(有大量空字符)
char week[][10] = { "Monday","Tuesday","Wednesday","Thursday", "Friday","Saturday","Sunday" };
-
使用字符指针数组表示(推荐)
字符串操作
搜索字符串末尾常用方法:
-
s 指向空字符
while(*s){ s++; }
-
s 指向空字符的下一个字符
while(*s++) ;
使用以上方法实现 strlen()
、strcat()
:
//my_strlen()
size_t my_strlen1(const char* s) {
char* p = s; //保存字符串首
while (*p) //p指向字符串末尾
p++;
return p - s;
}
size_t my_strlen2(const char* s) {
char* p = s;
while (*p++) //p指向字符串末尾的下一个字符
;
return p - s - 1;
}
//my_strcat()
char* my_strcat1(char* dest, const char* src) {
char* p = dest;
while (*p) //搜索字符串末尾
p++;
while (*p++ = *src++)
;
return dest;
}
指针
-
指针:存放变量的地址,unsigned类型的整数。
-
指针变量 :存放地址(指针)的变量。
-
&
: 引用、取地址。不能取寄存器变量地址,因为寄存器变量不在内存中。 -
*
: 解引用运算符,通过指针访问指针指向的对象。
声明
-
基类型 *指针变量名
-
同时声明多个指针变量时,
*
只和第一个变量名结合。int *ip; // 整型指针 double *dp; // double 型指针 float *fp; // 浮点型指针 char *cp; //字符型指针 char *p1, *p2, p3;//*p1 *p2是char型指针变量,p3是一个char型变量
只做声明而不初始化的指针是一种野指针,不能直接使用。
初始化
- 变量取地址
- 另一个指针赋值
int variable, a[6];
//变量取地址初始化指针
int *p1 = &variable;
int *p2 = a;
int *q;
q = p2;//另一个指针初始化指针
间接访问
访问一个变量可以直接访问、间接访问,只有需要间接访问时才使用指针。
- 间接访问:通过指向变量内存空间的指针访问变量(访问内存两次)
- 直接访问:通过变量名访问(访问内存一次)
#include <stdio.h>
int main(){
int a = 88;
int *p = &a; //定义指针并初始化,*p相当于a的别名
printf("直接访问: a = %d\n", a); //直接访问
printf("间接访问:*p = %d\n", *p); //间接访问
return 0;
}
*p
将p变量的内容取出,当做地址看待,并找到地址对应的内存单元。- **
*p
**做左值:存数据到对应的内存中 - **
*p
**做右值:取出内存中的内容
- **
野指针
未初始化、指向未知区域的指针是野指针。野指针并不会引发错误,但是操作野指针指向的内存区域会出现未定义的行为(不确定会发生什么)。
-
指针未初始化、没有有效地址空间的指针。
#include <stdio.h> int main() { int* ip1; *ip1 = 1000; //使用了未初始化的局部变量ip1 printf("%p", ip1); return 0; }
-
指针变量对应的内存不可访问——访问权限冲突,地址0~255留给操作系统使用。
#incldue <stdio.h> //编译成功,但是引发了异常: 写入访问权限冲突 int main() { int* ip2 = 10; //地址0~255留给操作系统使用,不要直接使用一个数值给指针变量赋值 *ip2 = 1000; printf("%p", ip2); return 0; }
空指针
变量声明时,如果没有确切的地址可以赋值,可以为指针变量赋 NULL
,eg:int *p = NULL;
,赋值为**NULL
** 的***p
就是空指针**。NULL
在C语言的标准库中的定义为**#define NULL ((void *)0)
**,所以赋值为 NULL
的指针一定会发生访问权限冲突(因为0~255的地址由操作系统保留)。
泛型指针
void *p
可以接收任意一种变量地址,但是不能间接引用自身,必须强制类型转换为具体的数据类型。
#include <stdio.h>
int main() {
int a = 66;
void* p = &a; //void *p 万能指针、泛型指针
//printf("%d", *p); //不允许使用不完整的类型
printf("%d", *(int *)p);//66 * 与()都是单目运算符,优先级相同,从右向左结合
return 0;
}
指针大小
指针的大小取决于编译器的位数,与指针类型无关。因为一个地址是32、64位的unsigned型整数,所以一个指针占4、8字节。
-
32位编译器,所有的指针都是32位(4字节)。
-
64位编译器,所有的指针都是64位(8字节)。
#include <stdio.h> int main() { int* p1; int** p2; char* p3; char** p4; printf("sizeof(p1) = %d\n", sizeof(p1)); printf("sizeof(p2) = %d\n", sizeof(p2)); printf("sizeof(p3) = %d\n", sizeof(p3)); printf("sizeof(p3) = %d\n", sizeof(p4)); printf("sizeof(void *) = %d\n", sizeof(void *)); return 0; }
指针 & 数组
-
数组名可以作为指针使用,它是指向索引值为0的元素的指针常量,在程序运行期间不能改变,不能被赋值。
-
指针可以作为数组名使用,即可以对指针使用
[]
运算符。由于指针是变量,可以用数组名给指针赋值。
⇒
\Rightarrow
⇒ 访问数组元素有两种方法:指针法 :*( )
;下标法 :[ ]
,且两者是等价的。
#include <stdio.h>
int main() {
int arr[] = { 0,1,2,3 };
int* p = arr;
printf("arr[2] = %d\n", *(arr + 2));//数组名作为指针使用
//arr++; //数组名是指针常量,程序运行期间不能被改变
printf("arr[2] = %d\n", p[2]); //指针作为数组名使用
return 0;
}
* & ++ - -
++
、--
与 *
优先级相同,从右向左结合。--
与 *
的组合同 ++
与 *
的组合:
*p++
$\Leftrightarrow $*(p++)
,表达式的值为*p
,副作用是 p 自增。(*p)++
表达式的值为*p
,副作用是 *p 自增。*++p
⇔ \Leftrightarrow ⇔*(++p)
表达式的值为*(p+1)
,副作用是 p 自增。++*p
⇔ \Leftrightarrow ⇔++(*p)
表达式的值为*p+1
,副作用是 *p 自增。
#include <stdio.h>
int main() {
int a1[] = { 2,4,6,8 };
int* p = a1;
int b1 = *p++; //b1 = *(p++) = 2
printf("a1[0]=%d, b1=%d, *p=%d\n", a1[0], b1, *p); //2 2 4
int a2[] = { 2,4,6,8 };
int* q = a2; //*q = 2
int b2 = (*q)++;//<=> b2 = *q; b2++; => b2 = 2, a2[0] = 3
printf("a2[0]=%d, b2=%d, *q=%d\n", a2[0], b2, *q); //3 2 3
int a3[] = { 2,4,6,8 };
int* r = a3; //*r = 2
int b3 = *++r; //b3 = *(++r) = 4
printf("a3[0]=%d, b3=%d, *r=%d\n", a3[0], b3, *r); //2 4 4
int a4[] = { 2,4,6,8 };
int* s = a4; //*s = 2
int b4 = ++*s; //b4 = ++(*s) = 3
printf("a4[0]=%d, b4=%d, *s=%d\n", a4[0], b4, *s); //3 3 3
return 0;
}
指针 & 一维数组
a[i]
⇔ \Leftrightarrow ⇔*(a+i)
,即arr[i] == *(arr+i) == p[i] == *(p+i)
。&array+1
:偏移过整个数组 array。
#include <stdio.h>
int main() {
int a[] = { 1,2,3,4,5,6 };
int* p = a; // <=> int *p = &a[0];
//访问数组元素的方法
printf("a[0]=%d\n", a[0]); //1
printf("*(a+0)=%d\n", *(a+0)); //1
printf("p[0]=%d\n", p[0]); //1
printf("*(p+0)=%d\n", *(p + 0));//1
//&数组名+1 => 偏移过整个数组
printf(" a = %p\n", a); //a = 00CFF790
printf("&a+1= %p\n", &a + 1); //&a+1= 00CFF7A8
printf("sizeof(a)=%d\n", sizeof(a)); //sizeof(a) = 24 = 00CFF7A8-00CFF790
return 0;
}
指针 & 二维数组
-
对于二维数组有:
a[i][j]
⇔ \Leftrightarrow ⇔*(*(a+i)+j)
,即arr[i][j] == *(*(arr+i)+j) == p[i][j] == *(*(p+i)+j)
。 -
行指针:
int *row = arr[i]
-
列指针:
int *col = &arr[0][j]
int a[3][4] = {{0,1,2,3}, {4,5,6,7}, {7,8,9,10}}; // 列指针遍历第2列所有元素 int* col = &a[0][1]; for (int i = 0; i < 3; i++) { printf("%d ", *( col + i * 4 )); }
-
二维数组本质是二级指针
#include <stdio.h> int main() { int a[] = { 1,2,3 }; int b[] = { 4,5,6 }; int c[] = { 7,8,9 }; int* p1 = &a, * p2 = &b, * p3 = &c; int* pointer_array[] = { p1,p2,p3 };//整型指针数组 //pointer_array[0][0] = *(pointer_array[0]) = **pointer_array = 1 printf("pointer_array[0][0] = %d\n", pointer_array[0][0]); printf("**pointer_array = %d\n", **pointer_array); return 0; }
字符指针 & 字符数组
字符指针初始化时可以赋值字符串常量;字符数组初始化时也可以赋值字符串常量,但是实现方式有区别:
- 使用
""
初始化的字符型常量都是**const char
** 型,存放在数据区,只能读不能写。 char *p = "String constant";
:将指针 p 指向数据区的字符串常量"String ocnstant"
的首地址,不能修改。char s[] = "String constant";
:相当于strcpy(s,"String constant");
操作将常量从数据区拷贝到另一片地址。
#include <stdio.h>
#include <string.h>
int main() {
char c[10] = "hello"; //等价于strcpy(c,"hello"); c为const char
char* p = "hello"; //把字符串型常量"hello"的首地址赋给p
c[0] = 'H';
//p[0]='H'; //引发了异常: 写入访问权限冲突。不可以对常量区数据进行修改
printf("c[0]=%c\n", c[0]);
printf("p[0]=%c\n", p[0]);
p = "world"; //将字符串world的地址赋给p,只是改变指针的指向
//c = "world"; //表达式必须是可修改的左值 => 无法对const char型的数组c赋值
puts(p); //world
return 0;
}
若取消 p[0]='H';
的注释,运行时会有异常——写入访问权限冲突;若取消 c="world";
的注释,会直接报错,无法编译。因为使用**""
**初始化的字符串常量是 const char
型,存放在数据区,这片区域只能读不能写,所以会有以上现象。
字符指针数组
char* weekdays[] = { "Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" };
[]
比 *
优先级高,先于 []
结合成为数组,再与 *
结合成为字符指针数组,即数组元素类型是 char *
指针运算
若指针变量 p 指向某一数组元素a[x],即 p = &a[x]
,那么有:
-
p++
、p+=1
、*(p+1)
都会使 p 访问到下一个元素;p--
、p-=1
、*(p-1)
使 p 访问上一个数组元素。-
p++
、p+=1
的副作用是改变了 p 的值,导致访问数组越界,同时可能导致指针 p 成为野指针。 -
p+1不会改变 p 的值,但是
*(p+i)
仍可能会发生访问数组越界,但不会导致指针 p 成为野指针。
-
#include <stdio.h>
int main() {
int a[6] = {1,2,3,4,5,6};
int* p, * q, * r;
p = q = r = &a[4];
printf("a[4] = %d, &a[4] = %p\n", a[4], &a[4]);//a[4] = 5, &a[4] = 001AFB78
//p++ 会改变 p 的值,导致访问数组越界,p成为野指针
p++; printf("p = %p,*p = %d\n", p, *p); //p = 001AFB7C,*p = 6
p++; printf("p = %p,*p = %d\n", p, *p); //p = 001AFB80,*p = -858993460
//q+=1 会改变q 的值,导致访问数组越界,q成为野指针
q += 1; printf("q = %p,*q = %d\n", q, *q); //q = 001AFB7C,*q = 6
q += 1; printf("q = %p,*q = %d\n", q, *q); //q = 001AFB80,*q = -858993460
//r+i 不会改变r的值,但是也可能发生访问数组越界,但不会成为野指针
r + 1; printf("r = %p,*r = %d\n", r, *(r+1));//r = 001AFB78,*r = 6
r + 1; printf("r = %p,*r = %d\n", r, *(r+2));//r = 001AFB78, * r = -858993460
return 0;
}
由以上例子可知:p++
、p+=1
、*(p+1)
都使得指针 p 访问到了后面的一个元素,都有可能发生访问数组越界,程序执行时 p+1
不会改变指针指向,但是 p++
、p+=1
改变了指针的指向,使得指针指向了数组外的未知区域,从而成为了野指针。
指针运算
-
p + 整数
p+i
、a+i
从当前位置 a[x] 向后偏移 i 个元素;p-i
、a-i
从当前位置 a[x] 向前偏移 i个元素。
-
p2 - p1
(指针的比较)- p1、p2当指向同一数组时,表示两个指针所指元素之间的相对距离(间隔几个元素)
- p1、p2指向两个普通变量时无意义
-
指针不能进行算术**
*、/、%
**,无法通过编译。int main() { int a[10] = { 1,2,3,4,5,6,7,8 }; int* p = &a[5]; printf("%p p:%d\n", p, *p); //6 //p++ p=p+1 指针指向下一个元素 p++; printf("%p p++:%d\n", p, *p);//7 p += 1; printf("%p p+1:%d\n", p, *p);//8 //p-- p=p-1 指针指向上一个元素 p--; printf("%p p--:%d\n", p, *p);//7 p -= 1; printf("%p p-1:%d\n", p, *p);//6 //从当前元素向前偏移(p-i)、向后偏移(p+i) p -= 2; printf("%p p-=2:%d\n", p, *p);//4 p += 4; printf("%p p+=4:%d\n", p, *p);//8 //p1-p2 表示指针所指元素之间间隔的元素个数 int* p1 = &a[2]; int* p2 = &a[6]; printf("p2 - p1 = %d\n", p2 - p1); //4 return 0; }
* & const
const
修饰指针时,不考虑指针的类型,将 const
*向右结合,被修饰的部分(p、p)变为只读。常用在函数形参内,限制指针所对应空间为只读,增加程序健壮性。
-
const
修饰*p
:指针指向内存中的值不可变,eg:const int *p
、int const *p
能修改p,不能修改*p
。int a = 1, b = 6; const int* p = &a; //*p = 8; //取消本行注释编译器报错:表达式必须是可修改的左值 p = &b;
-
const
修饰p
:指针指向的位置不可变,eg:int * const p
能修改*p
,不能修改p。 -
const
同时修饰*p、p
:指针指向的位置、位置中的值都不改变,eg:const int *const p
中的*p、p
两者都不能修改。
#include <stdio.h>
int main() {
int a[6] = {1,2,3,4,5,6};
const int* p = a; //*p不可变,p可变
//(*p)++; //表达式必须是可修改的左值
p++; printf("a = %p, &p = %p\n", a, p);//a = 0137FA54, &p = 0137FA58
int const* q = a; //*q不可变,q可变 <=>const int* q = a;
//*q = 2; //表达式必须是可修改的左值
q++; printf("a = %p, &q = %p\n", a, q);//a = 0137FA54, &q = 0137FA58
int* const r = a; //*r可变,r不可变
//r++; //表达式必须是可修改的左值
printf("a[0] = %d\n", a[0]); //a[0] = 1
*r = 2;
printf("a[0] = %d,*r = %d\n", a[0], *r);//a[0] = 2,*r = 2
const int* const s = a; //*r r都不可变
//*s = 4; //表达式必须是可修改的左值
//s+=1; //表达式必须是可修改的左值
return 0;
}
指针数组
-
存储元素全部是指针(地址)的数组
-
指针数组本质是二级指针
#include <stdio.h> int main() { int a = 1, b = 2, c = 3; int *p1 = &a; int *p2 = &b; int *p3 = &c; int *pointer_array[] = { p1,p2,p3 };//整型指针数组 //指针数组第一个元素 *pointer_array[0] = 1,二级指针 **pointer_array = 1 printf("*pointer_array[0] = %d\n", *pointer_array[0]); printf("**pointer_array = %d\n", **pointer_array); //pointer_array[0] = *(pointer_array+0) return 0; }
二级指针
赋值:一级指针取地址
- 要想在函数中改变变量的值,必须把变量的地址传进去
- 要想改变函数中指针变量的值,必须把指针变量的地址传进去
#include <stdio.h>
void change(int **pi,int *pj){
*pi = pj; //把一级指针的地址给二级指针,修改二级指针的指向
}
int main(){
int i = 4, j = 6;
int *pi = &i, *pj = &j;
printf("i=%d,*pi=%d; j=%d,*pj=%d\n", i, *pi, j, *pj);
change(&pi, pj); //pi是一级指针,对一级指针取地址是二级指针
printf("After changing:i=%d,*pi=%d,j=%d,*pj=%d\n", i, *pi, j, *pj);
return 0;
}
运行结果:
内存管理
堆、栈区别
- 栈空间在函数调用时创建,调用结束就销毁。不能使用一个指针变量访问被调函数的栈空间内的变量。
- 堆空间由**
malloc
**申请,若不用free
释放申请的空间,则在进程执行过程中一直可以访问。堆空间不会随子函数的结束而释放,必须自己释放。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* print_stack(){
char stack[] = "I'm a stack!";
puts(stack); //能正常打印
return stack;
}
char* print_heap(){
char write_to_heap[] = "I'm a heap!";
char* heap = (char*)malloc(sizeof(write_to_heap));
strcpy(heap, write_to_heap); //往刚分配的堆空间写数据
puts(heap);
return heap;
}
int main(){
char* stack, * heap;
stack = print_stack(); //函数调用创建了函数栈,栈空间会随着函数的执行结束而释放
puts(stack); //打印不出来,指针变量不能指向被调函数的栈空间内的变量
heap = print_heap();
puts(heap);
return 0;
}
运行结果:
内存泄漏
- 程序运行时动态分配的堆内存由于某种原因未释放、不能释放,成为系统垃圾,拖慢程序运行速度或使系统奔溃。
- 及时释放不用的内存块避免内存泄露
int *p = (int *)malloc(sizeof(int)); //1号内存
int *q = (int *)malloc(sizeof(int)); //2号内存
p = q; //p指向了q,导致没有指针指向1号内存,未及时释放而成为了垃圾
函数
- 作用:提高程序复用性、可读性,使程序更加模块化,分为库函数、用户自定义函数。
- 声明:告诉编译器函数的名称、返回值类型、形式参数的个数、类型、顺序。隐式声明默认返回值类型是整型。
- 定义:函数原型(函数名、返回值类型、形参)+ 函数体,指明函数要做什么。
- 调用:表明在此处执行函数。
- 栈帧:发生函数调用时,系统会在栈区开辟一片空间,用来存放函数的形参、局部变量,当函数调用结束时销毁。
声明函数时并不会为函数原型中的变量分配内存,当发生函数调用时才会给函数定义中的形参分配空间、初始化为实参表达式的值。
参数传递
实参:函数调用的括号中的内容,可以是常量、变量、表达式,但必须是确定的值。
形参:函数的自变量,用来接收调用该函数时传入的参数。本质是一个名字,不占用内存空间,发生函数调用时才分配内存单元,调用结束后释放。
实参与形参应该个数相等、类型匹配、顺序对应、一 一传递数据。实参 -> 形参的数据传递特点如下:
-
单向一次性的值传递(只能由实参 -> 形参传值,类型不能传递)
-
将实参的值复制给形参实现值传递,在函数内部不能修改实参的值。(传值不能改变实参的值)
#include <stdio.h> void swap(int arg_a, int arg_b) { //1 2 int temp = arg_a; //temp=1 arg_a = arg_b; //arg_a=2 arg_b = temp; //arg_b=1 } //形参arg_a arg_b交换了值,但实参a,b值不变 int main() { int a = 1, b = 2; swap(a, b); //将a=1,b=2复制给arg_a、arg_b, printf("a = %d, b = %d\n", a, b); //a = 1, b = 2 return 0; }
-
若实参是一个地址,则形参是指向实参所指对象的指针,在函数内部可以改变实参的值。(传址也是传值的一种,地址是一个十六进制的无符号数)
#include <stdio.h> void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } int main() { int a = 1, b = 2; int* p1 = &a; int* p2 = &b; swap(p1, p2); printf("a = %d, b= %d\n", *p1, *p2);//a=2 b=1 return 0; }
-
形参相当于局部变量,不能在函数内部再定义与形参同名的局部变量,否则会编译不通。
一维数组作形参
-
数组名作为实参传递给函数时,弱化为指针。传递的是数组,但是在函数中是使用指针遍历数组元素。
-
数组array做函数形参时,*char array[ ] ⇔ \Leftrightarrow ⇔ char array[10] ⇔ \Leftrightarrow ⇔ char array。
-
一维数组作形参传递时函数接收到的是数组名(一个地址),所以会丢失类型信息、数组长度,故一维数组作形参时,需要传递数组的长度。
#include <stdio.h> void print1(int b[]) { int i = 0; for (i = 0; i < sizeof(b) / sizeof(b[0]); i++) { //!!!!!!典型错误 printf("a[%d]:%d ", i, b[i]); } putchar('\n'); } void print2(int b[], int len) { int i = 0; for (i = 0; i < len; i++) { printf("a[%d]:%d ", i, b[i]); } putchar('\n'); } int main() { int a[4] = { 0,1,2,3 }; print1(a); //x64输出 a[0]:0 a[1]:1 x64:sizeof(b) / sizeof(b[0]) = 2 //x86输出 a[0]:0 x86:sizeof(b) / sizeof(b[0]) = 1 print2(a, 4);//a[0]:0 a[1]:1 a[2]:2 a[3]:3 return 0; }
-
好处:避免了复制所有数组元素;可以修改数组元素的值;操作更加灵活。
#include <stdio.h> void change1(char array[]){ //传递给形参array变为char * array[0] = 'H'; } void change2(char array[10]){ array[0] = 'h'; } void change3(char* array){ //多种赋值方法,操作更加灵活 *array = 'H'; *(array + 2) = 'L'; array[3] = 'L'; } int main(){ char c[10] = "hello"; change1(c); puts(c);//Hello 实参c是char型 change2(c); puts(c);//hello change3(c); puts(c);//HeLLo return 0; }
二维数组作形参
-
二维数组作形参传递时,可以忽略行的信息,不能忽略列的信息。
i_j_address = base_address + i * column * sizeof(element_type) + j * sizeof(element_type)
#include <stdio.h> int sum(int arr[][4], int n) { //n为行 int sum = 0; for (int i = 0; i < n; i++){ for (int j = 0; j < 4; j++){ sum += arr[i][j]; } } return sum; } int main() { int matrix[4][4] = { {1,2,3,4},{2,2,3,4} }; printf("sum(matrix)=%d\n", sum(matrix, 4)); return 0; }
-
传递列数不固定的二维数组——指针数组
指针作函数形参
在函数传参过程中,值传递不能改变实参的值,若传递实参的地址,可以用指针改变实参的值。
//指针作形参,求数组最大值最小值
#include <stdio.h>
void find_min_max(int* p, int len, int *min, int *max) {
*min = p[0];
*max = p[0];
for ( int i = 0; i < len; ++i ) {
if ( p[i] > *max )
*max = p[i];
else if ( p[i] < *min )
*min = p[i];
}
}
int main() {
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
int min, max;
find_min_max(arr, 10, &min, &max);
printf("min=%d,max=%d", min, max); //min=0,max=9
return 0;
}
return & exit()
- return:带着函数的返回值返回到函数调用处,返回值不是数组。
- exit( ):直接退出当前程序。
#include <stdio.h>
int test1(int arg) {
printf("test1()…arg=%d\n", arg);
return 0;
}
int test2(int arg) {
printf("test2()…arg=%d\n", arg);
exit(0); //执行后直接退出程序,不再返回程序调用处
}
int main() {
/*屏幕上输出:
test1()…arg=6
test1()返回值:0
*/
printf("test1()返回值:%d\n", test1(6));
printf("test2()返回值:%d\n", test2(8));//test2中的exit(0)执行后直接退出程序,所以该语句以及以下语句都不再执行
return 0;
}
作用域
全局变量
- 定义在函数外面的变量,存储在数据段,在整个进程的执行过程中始终有效。尽量不用!!!
- 全局变量的作用域是从定义开始到文件末尾
#include <stdio.h>
int g = 6; //全局变量g程序运行期间一直存在
void func1(int a) {
printf("func1 g=%d\n", g); //无论实参传多少始终输出g = 6
}
int main() {
printf(" main g=%d\n", g); //6
func1(1);
return 0;
}
局部变量
-
定义在函数内部,存储在函数的栈空间,函数调用时分配空间,执行结束时释放空间。
-
局部变量的作用域是块,即最近的一对
{}
,在{}
外不能访问。#include <stdio.h> int main() { { int local = 1; //在`{}`外不能访问变量local } //printf("local=%d", local); //取消注释会报错:local未定义 return 0; }
-
就近原则:若局部变量与全局变量重名,获取、修改的值为局部变量。
#include <stdio.h> int g = 6; //全局变量g void func1(int a) { printf("func1 g=%d\n", g); //全局变量g } int main() { int g = 8; //局部变量g printf(" main g=%d\n", g); // main g=8 局部变量g和全局变量g 同名 => 局部变量g(就近原则) func1(1); //func1 g=6 return 0; }
-
形参也相当于局部变量,与全局变量同名时同样遵循就近原则。
#include <stdio.h> int g = 6; //全局变量g void func2(int g) { //局部变量g (形参相当于局部变量,所以此处的g是func2函数栈空间内的同名局部变量g=1 printf("func2 g=%d\n", g); //func2 g=1(局部变量与全局变量同名,所以此处的g使用的是函数栈空间内的局部变量g的值) } int main(){ printf(" main g=%d\n", g); // 数据段全局变量g => main g=6 func2(1); //实参=1 => 形参g=1 return 0; }
递归
函数自身调用自身的操作,必须要有递归结束的条件。
#include <stdio.h>
int factorial(int n) {
return n>=2?( n * factorial(n - 1)):1; //递归结束条件:n==1
}
int main() {
printf("4!= %d", factorial(4));
return 0;
}
何时用递归:一个大问题可以分解为多个小问题,且小问题的求解方式和大问题一样,就可以将上述小问题的解合并为大问题的解。
注意:边界条件;递推公式
{ F ( 0 ) = 0 , n = 0 F ( 1 ) = 1 , n = 1 F ( n ) = F ( n − 1 ) + F ( n − 2 ) , n ⩾ 2 \left \{ \begin {matrix} F(0) & = & 0 & , & n=0 \\ F(1) & = & 1 & , & n=1 \\ F(n) & = & F(n-1) & + & F(n-2), & n \geqslant 2\\ \end {matrix} \right. ⎩ ⎨ ⎧F(0)F(1)F(n)===01F(n−1),,+n=0n=1F(n−2),n⩾2
-
递归方法会重复计算大量值;非递归则会保存重复计算的值。
#include <stdio.h> //递归 太多值被重复计算 long long fib1(int n){ return (n>=0&&n<=1)?n:fib1(n - 1) + fib1(n - 2); } //非递归 保存需要重复计算的值 long long fib2(int n) { int a = 0, b = 1; //F(n-2) F(n-1) if ( n == 0 ) return a; //F(0) else if ( n == 1 ) return b;//F(1) else for ( int i = 2; i <= n; i++ ) { int tmp_i = b + a; //第i项 F(n) = F(n-1)+F(n-2) //准备第i+1项的F(n-1) F(n-2) a = b; b = tmp_i; } return b; //F(n) } int main() { printf("%lld\n", fib1(8)); printf("%lld\n", fib2(16)); return 0; }
-
通项公式: a n = ( 2 / ( 5 + 1 ) − 1 / ( 5 + 1 / 2 ) n ) / 5 a_{n}=(2/ (\sqrt{5}+1)-1/ (\sqrt{5}+1/2)^{n}) / \sqrt{5} an=(2/(5+1)−1/(5+1/2)n)/5 ,由于浮点数是非精确值,不能使用通项公式求斐波那契数列
-
线性代数:由推导过程可知只需求得
( 1 1 1 0 ) n \begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} (1110)n 的值就可以知道斐波那契数列的某个值,时间复杂度为O(log n)。
{ F n + 1 = F n + F n − 1 F n = F n \left \{ \begin {matrix} F_ {n+1}& =& F_{n} &+&F_{n-1}\\ F {n} & = &F {n} & \\ \end{matrix} \right. {Fn+1Fn==FnFn+Fn−1⇒ ( F n + 1 F n ) = ( 1 1 1 0 ) ⋅ ( F n F n − 1 ) = ( 1 1 1 0 ) n ⋅ ( F 1 F 0 ) = ( 1 1 1 0 ) n ⋅ ( 1 0 ) \Rightarrow \begin {pmatrix} F_ {n+1}\\ F_ {n}\\ \end{pmatrix} = \begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} \cdot \begin {pmatrix} F _ {n}\\ F {n-1} \\ \end{pmatrix} =\begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} \cdot \begin {pmatrix} F_{1} \\ F_{0} \\ \end {pmatrix} =\begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} \cdot \begin {pmatrix} 1 \\ 0 \\ \end {pmatrix} ⇒(Fn+1Fn)=(1110)⋅(FnFn−1)=(1110)n⋅(F1F0)=(1110)n⋅(10)
内联函数
在实现函数体时使用**inline()
**修饰的函数就是内联函数,在调用内联函数时,内联函数体内容直接替换函数调用。
- 内联函数只适合函数体内结构简单的函数使用(没有while、switch,不能是递归函数)
inline
修饰的函数体必须在头文件中。若inline
修饰的函数定义在头文件中,实现在其他文件中,会发生链接错误。(因为inline在编译阶段就发生了替换,压根没有进行链接)
结构体
C语言的结构体像其他高级语言的类,eg:定义一个“学生”
typedef struct student {
int num;
char name[20];
bool gender;//0-man 1-woman
int age;
}Student;
初始化
Student s1 = { 001,"Shine",0,18 };
Student s2 = { 002,"Lisa",1 }; //未初始化的成员赋值为0
Student s3 = s1;
访问成员
.
:结构体变量、结构体指针变量都可以使用。->
:当使用到结构体指针时用,相当于(*p).
。
作函数参数
- 值传递 —— 结构体作函数参数时会复制结构体的全部数据
- 使用结构体指针作函数参数避免复制结构体的全部数据
#include <stdio.h>
#include <stdbool.h>
typedef struct student {
int num;
char name[20];
bool gender;//0-man 1-women
int age;
}Student;
void print1(Student s) { //结构体作参数(值传递)拷贝整个结构体数据
printf("%d %s %d %d\n", s.num, s.name, s.gender, s.age);
}
void print2(Student* s) { //避免复制全部数据使用结构体指针
printf("%d %s %d %d\n", (*s).num, (*s).name, s->gender, s->age);//s->num <=> (*s).num
}
int main() {
//初始化
Student s1 = { 001,"Shine",0,18 };
Student s2 = { 002,"Lisa",1 }; //未初始化的成员赋值为0
//赋值 .访问成员
Student s3;
s3 = s2;
s3.age = 16;
printf("s3:%d %s %d %d\n", s3.num, s3.name, s3.gender, s3.age);//s3:2 Lisa 1 16
print1(s2); //2 Lisa 1 0
print2(&s3);//2 Lisa 1 16
return 0;
}
何时分配内存
-
定义结构体类型之后,未定义结构体变量之前 只是增加了一种数据类型,不会分为它配内存。
-
定义结构体指针之后申请了一个指针,并告诉系统这个指针是干什么用的,至于指针指向的地址并不确定。由于指针本身要占用内存,只分配了指针所需大小的内存,这个分配是使用
malloc()
手动分配的,系统并不会自动给它分配空间。 -
定义了结构体变量系统会自动为该变量分配内存空间。
typedef struct Lnode{ Elemtype data; struct Lnode *next; }* LinkList, Lnode; //指向Lnode结构体的指针别名是LinkList,结构体别名是Lnode int main(){ LinkList L; //声明了一个Linklist型结构体指针 L = (Lnode *)malloc(sizeof(Lnode)); //手动使用malloc为指针分配空间 if (L==NULL) return false; //内存不足,分配结点失败 L->next = NULL; //头结点之后没有其他节点,空的带头结点单链表形成 return 0; }
枚举
- 枚举类型的
枚举元素
按常量处理,不能取成员变量。 - 每一个枚举元素都代表一个整数,从 0 开始,可以在定义中为枚举元素指定值。
- 枚举元素可以用来判断比较 eg:
if(workday == monday)
、if(workday > sunday)
#include <stdio.h>
typedef enum day {
monday = 1,
tuesday,
wednesday,
thursday,
friday,
saturday = 7,
sunday
} Week;
void print_week(Week * e){
printf("%d %d %d %d %d %d %d\n", monday, tuesday, wednesday,
thursday, friday, saturday, sunday);
}
int main() {
int a = 7;
Week weekend;
print_week(&weekend); //1 2 3 4 5 7 8
weekend = (enum day)a; //类型转换
//weekend = a; //错误
printf("weekend:%d", weekend);
return 0;
}
共用体
- 多个变量使用同一块地址空间,但是任何时候只能有一个成员有值。共用体变量中起作用的成员是最后一次被赋值的成员。
- 共用体大小由最大的成员大小决定
- 共用体+结构体:共用体可以出现在结构体定义中,结构体也可以出现在共用体定义中
- 共用体+数组:数组可以作为共用体成员,可以使用共用体数组
#include <stdio.h>
union Data{
int i;
float f;
char str[20];
};
int main( ){
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i); //10
data.f = 220.5;
printf( "data.f : %f\n", data.f); //220.500000
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);//C Programming
return 0;
}
位域
-
把一个字节中的二进制位划分为几个不同的区域,并给划分的区域命名、指定区域位数,这样的数据结构称为“位域”。
-
定义与结构体相仿,使用和结构成员使用相同
struct 位域结构名 { 类型 位域域名1:宽度; 类型 位域域名2:宽度; }; 位域变量名.位域名 位域变量名->位域名
-
典型应用:使用1位二进制位存放开关量
#include <stdio.h> typedef struct SwitchValue { unsigned int switch_value1 : 1; //给划分的区域命名、指定宽度为1bit unsigned int switch_value2 : 1; }status; int main() { printf("%d", sizeof(status)); //4 return 0; }
-
注意事项:
- 位域可以是无名位域,但此时只能用来填充、调整位置,并不能使用
- 位域宽度不能超过他的成员变量使用的数据类型的宽度
C标准库
<stdio.h>
fgets()
char *fgets(char *s, int size, FILE *stream)
- 获取一个字符串,提前预留一个
'\0'
的位置,保证获取到的一定是有效的字符串,不会再产生越界,弥补了gets的缺点。 - 存储字符串的空间足够时读取
'\n'
,空间不足时舍弃'\n'
。
#include <stdio.h>
int main() {
char str[10];
fgets(str, sizeof(str), stdin);
printf("%s", str);
return 0;
}
运行结果:
fputs()
int fputs(const char *str, FILE *stream)
- 返回值:成功——非负值;失败—— -1。
- 输出字符串后不添加
\n
。
<stdlib.h>
内存管理
malloc
void* malloc( size_t size );
- 分配成功时,返回指向新内存的指针。
为避免内存泄漏,在使用完该区域之后,必须使用free()
释放它、realloc()
重新分配。 - 分配失败时,返回空指针。
calloc
void* calloc( size_t num, size_t size );
- 分配
num
个size
大小字节的空间并初始化为0- 分配成功:返回该片空间的首地址
- 分配失败:返回空指针
realloc
void *realloc( void *ptr, size_t new_size );
- 重新调整已分配的内存块大小,调整成功返回首地址,否则返回空指针。
- ptr 应该指向先前已经分配的内存块
free
void free( void* ptr );
释放 malloc()
、calloc()
、realloc()
分配的内存,有可能造成指针悬空。
随机数
void srand( unsigned seed );
设定伪随机数种子
int rand();
生成伪随机数
//生成十个伪随机数
srand(time(NULL));
for (int i = 0; i < 10; i++)
printf("%d\n", rand());
system
int system( const char *command );
执行一条系统命令、调用系统工具,如cmd、mspaint(画图工具)、notepad(记事本)。
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
int main() {
//system("cmd"); //唤醒Windows记事本
//system("mspaint"); //唤醒画图
//打印时间
int h, m, s;//时分秒
printf("输入时分秒:\n");
scanf("%d %d %d", &h, &m, &s);
for (; h < 24; h++) {
for (; m < 60; m++) {
for (; s < 60; s++) {
printf("%02d:%02d:%02d", h, m, s);
Sleep(1000); //windows.h
system("cls"); //stdlib
}
}
}
return 0;
}
<string.h>
memset
void *memset( void *dest, int ch, size_t count );
-
按字节赋值 。由于0的补码全是0,-1 的补码全1,所以可以用来给数组的所有元素赋0、-1。
memset(a, 0, sizeof(a)); //赋初值0 memset(a, -1, sizeof(a)); //赋初值-1
-
用于初始化一片内存,初始化任何数组(字符数组、整型数组、浮点型数组、结构体数组)
typedef struct Student{ int num; int age; }student; student s[4]; memset(s,0,sizeof(s));
strlen
size_t strlen( const char *str );
-
返回字符串数组有效长度,遇
\0
即停止,包括\n,不包括\0。#include <stdio.h> #include <string.h> int main(){ char str1[] = "String\n hello"; // 13 char str2[] = "String\0 hello"; // 6 printf("str1: %d\nstr2: %d", strlen(str1), strlen(str2)); //13 6 return 0; }
strcpy
char *strcpy( char *dest, const char *src );
把 src 指向的字符串复制给 dest ,不检查数组是否会越界,所以需要在确保数组不会越界时再使用 strcpy()
,否则使用 strncpy()
。
const char *src
说明src
指向的内容不会被修改,作传入参数。 指向的内容 有const
修饰时,可以使用字符串常量代替字符串数组。char *dest
表明通常会在函数内部修改dest
指向的内容,作传入传出参数。
strncpy
char *strncpy( char *dest, const char *src, size_t count );
从字符串src
复制一定数量的字符到dest
:
- 若
count ≤ strlen(src)
,不会向dest
中写入\0
; - 若
count > strlen(src)
,会向dest
中写入\0
,直到够 count 个字符。
#include <stdio.h>
#include <string.h>
int main() {
//strcpy不会进行越界检查,确保不会越界时再使用
char s1[10], s2[10];
strcpy(s1, "hello.");
puts(s1); //hello.
strncpy(s2, "hello", 3);//s2:hel
strncpy(s2, "hello", 5);//s2:hello
strncpy(s2, "hello", 6);//s2:hello\0
strncpy(s2, "hello", 8);//s2:hello\0\0\0
return 0;
}
strcmp
int strcmp( const char *str1, const char *str2 );
按字典序比较 str1与 str2 的ASCII值,并返回一个值
- 返回值为 -1:str1 < str2
- 返回值为 0:str1 = str2
- 返回值为 1:str1 > str2
strcat
char *strcat( char *dest, const char *src );
- 把
src
的内容添加到dest
的末尾,不会做边界检查,确定不会发生越界、溢出时使用,否则使用strncat()
。 - 始终都会在添加字符结束后写入
'\0'
,即便会造成溢出也会写入(例如取消下面代码中的strncat(s1, "cat", 3);
)
strncat
char *strncat( char *dest, const char *src, size_t count );
- 把
src
的内容中的至多 count 个字符添加到dest
的末尾, - 始终会向末尾添加
'\0'
- 惯用法:
sizeof(dest) - strlen(dest) - 1
(提前预留'\0'
的位置)
#include <stdio.h>
#include <string.h>
int main() {
char s[12] = "cat";
strcat(s, " +str");
puts(s); //cat +str
char s1[10]="hello";
strncat(s1, "cat", 2);//helloca\0\0\0
//hellocacat\0 取消下行注释会发生异常,因为s1能放10个字符,但是向s[9]写入't'后仍会继续写入一个'\0'
//strncat(s1, "cat", 3);
//strncat惯用法: sizeof(s) - strlen(s) - 1 (-1是为了预留'\0'的位置)
char a[] = "++++";
strncat(s, a, sizeof(s) - strlen(s) - 1);
puts(s);
return 0;
}
其他
编译过程
- 预处理 :
gcc -E *.c -o *.i
- 取消注释、添加行号、保留
#pragma
编译器指令 - 处理条件编译指令
#if
、#ifdef
- 宏替换,替换宏定义、(含参)宏函数(文本替换)
- 展开
#include <xxx.h>
包含的头文件(复制到对应位置),可以展开任意文件,且不检查语法错误,*.c
文件变成*.i
文件。
- 取消注释、添加行号、保留
- 编译 :
gcc -S *.c、*.i -o *.s
- 对单个文件逐行进行词法、语法、语义分析,最为耗时
- 生成汇编代码文件
- 汇编 :
gcc -c *.c、*i、*s -o *.o
- 汇编代码转换位二进制文件,
*.s
文件变为*.obj
目标文件。
- 汇编代码转换位二进制文件,
- 链接 :
gcc *c、*i、*s -o *.exe
- 引入库文件
- 通过链接把多个目标文件(*.obj)关联到一起,解决外部内存地址问题 (一个文件的变量使用其他文件的变量)
每个项目都会被编译为一个可执行文件,方便解决错误。
命令行参数
程序开始运行时,操作系统会调用 main()
函数,它分为:
-
不含参数的
main(void)
-
含参
main(int argc, char *argc[])
argc
:执行命令时传递的参数个数*argv[]
:要传递的参数,第一个参数是*argv[0]
,表示生成的可执行程序的路径,*argv[i]
表示要传递的第 i+1 个参数- 传参方式:
可执行程序路径 参数2 参数3
#include <stdio.h> int main(int argc, char *argv[]){ printf("argc = %d\n", argc); for (int i = 0; i < argc; i++){ puts(argv[i]); } return 0; } /*执行:./a.out hello 输出:./a.out hello */
宏函数
#define
定义的宏函数效率比普通函数高,这是因为普通函数使用时有诸多开销:函数调用、保存寄存器值、传参、保存下一条指令地址、返回调用处、传递返回值、恢复寄存器值。
- 左括号紧贴宏函数名,宏函数的整个表达式加
()
- 含参宏函数的每个参数都应该加
()
- 多语句宏函数使用
do{} while(0)
实现只执行一次的效果 - 注意宏函数中多次
++
、--
副作用
#include <stdio.h>
#define FUNC(x) x + x * x
#define FUNC1(x) (x + x * x)
#define FUNC2(x) (x) + (x) * (x)
#define FUNC3(x) ((x) + (x) * (x))
#define HELLO() printf("Hello ");\
printf("world\n")
#define HELLO1() do{ printf("Hello ");printf("world\n");}while(0)
int main(void) {
int a = 4;
int b = 6;
printf("a=%d b=%d\n", a, b);
//整个宏函数表达式外应该有(),否则可能会出错
printf("FUNC(a) = %d\n", 2 * FUNC(a)); // => 2 * a + a * a = 24
//含参宏函数的每个参数应该加(),否则可能出错
printf("FUNC1(a+b) = %d\n", FUNC1(a+b)); // => a+b + a+b * a+b = 44
printf("FUNC2(a+b) = %d\n", FUNC2(a+b)); // =>(a+b) + (a+b) * (a+b) = 110
//宏函数多次副作用,不要使用++、--
printf("FUNC3(++b) = %d\n", FUNC3(++b)); //156 ((++b) + (++b) * (++b))
//多语句宏函数
putchar('\n');
if (0)
HELLO(); //=> if (0) printf("Hello "); printf("world\n"); =>world
//2.do{ }while(0)
HELLO1(); //do{ printf("Hello ");printf("world\n");}while(0);
return 0;
}
const
-
const修饰的常量不能用来指定数组长度
-
const定义的常量可以使用指针修改
#include <stdio.h> int main() { const int a = 66; //int arr[a]; //表达式必须含有常量值 int* p = &a; *p = 11; printf("a=%d", a); //11 return 0; }