C语言知识总结
前置知识:
VS基本操作
1.创建解决方案,并管理多个项目,每个项目分为头文件.h 源文件.c 资源文件:配置文件、音频、视频、图片等
2.通过打断点,逐过程或逐语句调试程序。
3.记住内存的大小单位:
bit :1位
byte(字节):8位(是最小的寻址单位)
kb:210byte
MB:220byte
GB:230byte
TB:240byte
4.地址:
大端表示法:高地址存放低有效位
小端表示法:低地址存放低有效位
程序是如何生成的
将.c 与.h 文件预处理后生成 .i 的文件,将该文件进行编译生成 包含汇编代码的 .s文件,将该文件进行汇编后生成 含目标代码的 .o文件,将该文件与 库文件 与引导代码 进行链接操作生成可执行程序。
预处理#:
include 将头文件包含的内容copy到对应的位置
宏定义#:简单的文本替换,且宏函数效率高
define N 5
define F(X) (1+(x)*(x) //带参数的宏(宏函数)
编译:
把C语言源代码翻译成汇编代码
汇编:
把汇编代码翻译成对应平台的机器代码
链接
将库函数、操作系统的引导代码以及目标文件,链接在一起,生成可执行文件
进程的虚拟空间
Kernel(内核) 最上端
Stack(栈)(向下扩容)
Heap(堆)(向上扩容)
Code(代码)
Data(数据)
最低端地址有一块空出,有特殊用途,不可访问(代表空指针null)
C基础知识
1.变量与常量
变量:在程序运行过程中可以发生改变的量
变量的本质:一块内存空间
变量三要素:变量名、数据类型、值
常量:在程序运行期间不可以发生改变的量
两种:宏定义define (直接在预处理阶段替换成5),在编译阶段就知道常量大小
const修饰:const int M =5;(M代表一片内存空间,这片空间不能修改),在编译阶段不知道具体大小,且不能作为数组长度、case标签
2.标识符和关键字
标识符:为变量、函数和宏起的名字.
规则:只能包含字母,数字和下划线
不能以数字开头
规范:单词间下划线分隔 symbal_table
驼峰命名法:symbalTble
要做到见名知义
注意:标识符区分大小写
标识符不能与关键字冲突(如int auto)
3.格式化输入和输出
格式化输出printf
该函数的原型为:
int printf(格式化字符串,表达式1,表达式2,...)
作用:显示格式串中的内容,并且在字符串指定的位置插入后面表达式的值
格式化字符串:
普通字符-直接输出
转换说明:以%开头的,表示一个占位符,打印时会以后面表达式的值替换这些占位符(1.以何种方式解释内存区域(编码) 2.控制输出格式)
转换说明格式:%m.px
m(minimal field width) 最小字段宽度 ,控制输出格式,表明要显示的最少字符的数量
p(presion)精度,当x为d时:p代表要显示数字的最小个数(必要时会在前面补0),若省略,默认为1 当x为f时:p代表小数点后面数字的个数(默认为6),对齐方式默认都是右对齐,加上-号可以指定为左对齐
x:以何种方式解释内存区域,如d:以十进制的形式输出整数, f:输出浮点数
格式化输入scanf
原型:
int scanf(格式化字符串,表达式1,表达式2,..)
本质:是一个模式匹配函数,试图把你输入的字符与格式化字符串进行匹配.
工作原理:从左到右依次匹配格式化字符串,如果匹配成功,则继续匹配后面的字符串,如果匹配失败,则立即返回
转换说明:表示匹配规则、表示如何将字符数据转换成二进制数据
格式化字符串:普通字符1.空白字符(空格,\t,\n,\v...),可匹配任意多个空白字符,包括零个 2.其它字符,每个字符精确匹配
转换说明
注意:scanf在处理%d,%f(进行数值匹配时),会读取前置的空白字符
4.基本数据类型
整数类型
分为无符号整数与有符号整数
short (int)
unsigned short (int)
int
unsigned (int)
long (int)
unsigned long (int)
long long (int)
unsigned long long (int)
注意:C语言没各个整数类型规定大小,但规定了各种整数类型的最小大小,shor <= int <=long <= long long
64位机器上整数类型的取值范围:
字节 最小值 最大值
short 2 -215=-32768 215 -1 =32767
unsi..short 2 0 216 -1=65535
int 4 -231 231-1
unsi..int 4 0 232
long 8(>=4bytes)
unsi..long 8
long long 8(>=8bytes)
unsi..longlong 8
编码(***重要)
无符号整数(用十进制转化为二进制)
有符号整数(用补码),原因--让加法器作减法运算
底层的数学原理:模运算
计算某个数的负数的补码:(口诀)
该数的补码,全部取反(包括符号位),最后一位加1,结果为负数的补码
该数补码 0011 0100
全部取反 1100 1011
取反后加1 0000 0001
负数补码 1100 1100
整数常量:
表示方式:十进制、八进制(以0开头)、十六进制(以0x开头)
类型:U(u) L(l) LL(ll) 混合使用
读写整数:
%d:有符号十进制整数
%u:无符号十进制整数
%o:无符号八进制整数
%x:无符号十六进制整数
读写短整数,在u,o,x,d前面添加h
读写长整数,在u,o,x,d前面添加l
读写长整数,在u,o,x,d前面添ll
浮点数
类型:float(4字节)、double(8字节)、long double(用于高精度数值计算中)
编码:(IEEE 754)
float中--sign(符号位)占1位,Exponent(指数)占8位,Fraction(小数部分)占23位。分为三个特征值与归约数、非归约数。
浮点数常量:
表示方法-要么包含字母E(e),要么包含小数点
57.0 5.70e1(e1表示10的1次方)
浮点数常量的默认类型double,若需要表示float类型,则需要在浮点数常量后面添加F(f)
读写浮点数:
%f: float
%lf: double
字符数据类型
char类型的数据只占1个字节,并且采用ASCII编码表示,ASCII编码用7位表示128个字符(最高位0)
记几个特殊:'\0' :空字符 ' ' = 32 '0' = 48 'A' = 65 'a' = 97
处理:C语言把字符类型当作整数来处理,可以进行算术运算、比较运算
转义序列:不能直接输入的字符
字符转义序列:
\n (newline) \t (horizonfal tab) \\ (backslash) \ ' (single quote)
\ "(double quote)
数字转义序列:
八进制形式:\开头后面接最多三个八进制数
十六进制形式:\x开头后面接十六进制数
字符处理函数:
字符分类函数、字符操作函数、
读写字符:
scanf/printf 配合%c来读写字符
注意:%c不会忽略前面的空白字符
解决办法:在%c前加空格,就可以匹配任意多个空白字符
scanf(" %c",&ch);
ch = getchar()/putchar(ch):效率比scanf/printf高
惯用法:读取一行剩余的字符(包括'\n')
while (gettchar() != '\n')
; //读取一行剩余的字符(包括'\n')
布尔类型
定义在stdbool.h 中
bool类型本质上是无符号整数
给bool类型变量赋值,非零会转化成true,零会转换成false.
5.类型转换
1.隐式类型转换:
原因:计算机硬件只能对相同类型的数据进行运算,当给定的数据类型与需要数据类型不匹配时,会进行隐式类型转换
表示范围(由小到大)char,short->int->long->long long->float->double,当小类型与大类型一起运算时,结果会转换为大类型
signed->unsigned 注意-不要将有符号整数和无符号整数一起参与运算
2.显式类型转换:
格式:(type_name) expression;
计算浮点数的小数部分
提高代码的可读性
让程序员对类型转换进行精确地控制
避免溢出
6.给类型起别名
typedef type_name alias;
typedef int YEAR;
好处:
提高代码的可读性
提高代码的可移植性
typedef和宏定义之间的区别:
宏定义(define)是在预处理阶段进行处理的(简单的文本替换),编译器是不能识别宏定义
编译器能够识别typedef的别名,若发生错误,编译器能给出一些友好提示.
7.sizeof运算符
作用:计算某以类型数据所占内存空间的大小(以字节为的单位)
格式:sizeof(type_name)
注意:sizeof运算符是在编译期间进行运算,因此它是一个常量表达式.所以它可作为数组的长度,也可以当作case后的标签.
8.表达式与运算符
表达式:计算某个值的公式,最简单的表达式:变量、常量.
运算符:连接表达式,创建更复杂的表达式.
算术运算符
赋值运算符
关系运算符
比较运算符
逻辑运算符
位运算符。。
两个属性:优先级、结合性
1.算术运算符 + - * / %
注意:+ - * /可以用于浮点数,但%要求两个操作数都是整数
两个整数相除,其结果是整数
i%j的结果可能为负,符号和i是相同.满足公式:i%j = i -(i/j)*j
2.赋值运算符
简单赋值:= v=e,把表达式e的值赋值给变量v(副作用),整个表达式的值就是赋值后变量V的值.
注意:赋值过程中可能会发生隐式类型转换. i,e int i = 3.14
从右向左结合 i = j = k = 3;<=> i=(j=(k=3))
复合的赋值运算符:
a += b a = a+b
a -= b a = a-b
a *= b a = a * b
a /= b a = a/b
3.自增和自减运算符
自增: i = i+1,i += 1
自减:i = i-1,i -= 1
C语言专门提供了自增和自减运算运算符:++,--
i++ :表达式的值为i,副作用是i自增.
++i:表达式的值为i+1,副作用是i自增.
注意:
i = i++; (会产生未定义行为) 即如a[i] = b[i++]
i--:表达式的值为i,副作用是i自减
--i:表达式的值为i-1,副作用是i自减
4.关系运算符 < , <= , >,>=
其运算结果要么为0,要么为1.
注意:
表示范围:i<j<k:先计算 i < j,其结果要么为0,要么为1,然后再计算(0,1)与k的大小 j > i && j<k
5.判断运算符
== ,!= 其结果要么为0,要么为1
6.逻辑运算符
&&, || , ! (与、或、非)
注意:
&&与||会发生短路现象
a && b :先计算表达式a的值,若a为false;则不会计算表达式b的值.
a || b :先计算表达式a的值,若a为true;则不会计算表达式b的值
好处:方便写程序
位运算符(***重要)
<< , >> , & , | , ^ ,~
1.移位运算符
i << j //将i左移j位,在右边补0
若左边没有发生溢出,左移j位,相当于乘以2j
i >> j //将i右移j位,若i是无符号整数或者非负数,则左边补0;若i为负数,它的行为是由具体实现决定的,有的补0,有的补1
注意:为了可移植性,不要对有符号整数进行移位运算
不存在溢出情况,右移j位,相当于除以2j (向下取整)
2.按位运算符
short i = 3 ,j = 4;
~i:按位取反(1变0,0变1),结果为-4
i & j: 按位逻辑与(&),某位 只有两个都为1,结果才为1,否则为0
i 0011
j 0100
i&j 0000
i | j:按位逻辑或( | ) ,某位 只要有一个1,结果就为1,否则为0
i&j 0111
i ^ j:按位 异或(^),某位相同为0,不同为1
i^j 0111
异或的性质:
a^0 = a;
a^a = 0;
a^b = b^a //交换性
a^(b^c) = (a^b)^c //结合性
//可以用来加密、解密
//加密
m^k = c //m:message k:key c:crytograph(密码本)
//解密
c^k = m
按位运算符的应用:
1.如何判断一个数是否是奇数
bool is_odd(int n){
return n & 0x1;
}
2.如何判断一个整数是否是2的幂
bool isPowerof2(unsigned int n){
unsigned int i = 1;
while (i<n){
i <<= 1;
}
// i >= n,并且i是2的幂
return i == n;//若n是2的幂,则会返回true
}
3.给定一个不为0的整数n,找出值为1且权重最低的位
n&(-n)
如:
0011 0100
& 1100 1100
0000 0100 输出结果为4,说明值为1且权重最低的位是第三位
也可以用来判断整型数据二进制中有多少位1
int main() {
unsigned int n;
int count = 0;
scanf_s("%u", &n);
while (n > 0) {
n = n &(n-1);
count++;
}
printf("%d\n", count);
return 0;
}
4.给定一个整数数组,里面的数都成双成对,只有一个数例外,请找出这个数?
int findSingleNumber(int arr[],int n){
int singleNum = 0;
for (int i = 0;i<n;i++){
singleNum ^= arr[i];
}
return singleNum;
}
9.选择语句
1.if语句
格式1:if(expr) statement
格式2:if(expr) statement1
else statement2
if/else嵌套
if/ else if /else 级联式if语句
2.switch语句
格式:
switch(expr){
case const-expr: statements;
break;
case const-expr: statements;
break;
......
default: statements;
break;
}
注意:
①expr必须是整数类型int 或 (char型)
②case后面的值必须在编译期间求得它得值
③不能有重复标签
④多个分支标签可以共用一组语句
switch(grade){
case 4:case 3:case 2:case 1:
printf("passing\n");
break;
case 0:
....
}
⑤可以省略break语句,可能会出现case穿透现象
级联式if语句和switch语句的比较
①级联式if语句比switch语句更加通用
②switch语句的可读性比if语句强
③switch语句的执行效率比级联式if要高
3.条件运算符(三目运算符)
? :
格式: expr1 ? expr2 : expr3
计算步骤: 首先计算表达式expr1的值,若expr1非零,则计算expr2的值,并把expr2的值当作整个表达式的值;若expr2为零,则计算expr3的值,并把expr3的值当作整个表达式的值.
结合性:从右向左结合
expr1 ? expr2 :(expr3 ? expr4 :(expr5 ? expr6 : expr7))
10.循环语句
1.while语句
格式: while(expr) statement (expr:控制表达式,statement:循环体)
2.do语句
格式: do statement while(expr);
do语句与while语句的唯一区别:当初始条件不成立时,while语句不会执行循环体;do语句会执行一次循环体
3.for语句
格式:for(expr1;expr2;expr3) Statement (statement:循环体)
expr1:初始化表达式,只会执行一次
expr2:控制表达式,若expr2的值非零,则执行循环体,若expr2的值为0,则退出循环
expr3:执行循环体之后,要执行的步骤
注意事项:expr1,expr2,expr3都可以省略,若省略expr2,默认值为true
惯用法:for(;😉{.....} 无限
while(1){...}
11.跳转语句
1.break语句
作用:跳出switch、循环语句
最常见于跳出while(1)这样的无限循环
注意:
当switch语句,while,do,for嵌套时,break只能退出包含break语句的最内层嵌套
2.continue语句
continue语句与break语句的区别
①break可以用于循环,switch语句,continue只能用于循环语句
②break是跳出整个循环,continue语句跳转到循环体的末尾
3.goto语句
goto语句可以在函数内进行任意跳转(只能在同一个函数内进行跳转)
尽量少用goto语句,很容易出现bug,只有当其它方式实现不了时,才考虑使用goto语句.
格式:goto 标签
使用场景:
①跳出外层嵌套
while(..){
switch(..){
..
goto loop_done;
...
}
...
}
loop_done: //goto跳到这里
.......
②错误处理
if(...){
goto error_handle;
}
.....
error_handle:
...//进行错误处理
C中阶知识
1.数组
前置知识:
数组的模型:连续的一片内存空间,并且这片连续的内存被分为大小相等的小空间.
数组通过存储同一种类型的数据来划分大小相等的小空间,这样可以实现随机存储元素(在o(1)的时间复杂度内访问任一一个元素)
由于寻址公式:i_addr = base_addr + i*sizeof(elem_type) 中i是从0开始的,若从1开始,则会变成(i-1) * sizeof(elem_type) ,每一次寻址都会多一次减法运算,且浪费一个元素的内存空间,故在大多数语言中,数组的索引都是从0开始的
数组的效率一般会优于链表:
①数组可以更好地利用cpu地cache(预读,局部性原理)
②数组只需要存储数据,链表不仅仅用存储,还要存储指针域
1.数组的声明
elem_type arr_name[size];
注意事项:size必须在编译时能够确定大小
2.数组的初始化
3.对数组使用sizeof运算符
#define SIZE(a) (sizeof(a) / sizeof(a[0]))
4.多维数组(二维数组)
逻辑上类比 矩阵 int matrix[3] [4] 3行4列
二维数组的内存空间是连续的(行优先)从第0行到第2行
二维数组的初始化:
int matrix[3][4] = {0};
int matrix[3][4] = {{1,2,3,4},{2,2,3,4},{3,2,3,4}};
int matrix[3][4] = {{1,2,3,4},{2,2,3,4}};
int matrix[][4] = {{1,2,3,4},{2,2,3,4},{3,2,3,4}};
注意:只能省略行的大小
常量数组:
const int arr[10] = {0,1,2,3,4,5,6,7,8,9};
const 表明数组的元素不能够发生改变
作用:存放一些静态数据
2.伪随机函数
随机数
头文件:<stdlib.h>
void srand(unsigned seed);
//seed :种子值 以seed的值播种 rand()所用的随机数生成器,一般只播种一次随机数生成器,在程序开始到rand()的调用前,不应重复播种
srand((unsigned)time(NULL));//以当前时间为随机数生成器的种子,注意不能没有unsigned,若无则每次生成的都是一样的随机数序列
int rand(); //生成随机数,返回值为随机整数值
头文件:<time.h>
time_t time(time_t* timer) //函数原型
//功能:获取当前的系统时间,返回的结果是一个time_t类型,其实就是一个大整数,其值表示从CUT(Coordinated Universal Time)时间1970年1月1日00:00:00(称为UNIX系统的Epoch时间)到当前时刻的秒数。然后调用localtime将time_t所表示的CUT时间转换为本地时间(我们是+8区,比UTC多8个小时)并转成struct tm类型,该类型的各数据成员分别表示年月日时分秒。
time(NULL) //一般用法
3.函数
数学上的函数:必有参数,必须要有返回值,且不会产生副作用
C语言中的函数:可以没有参数,也可以没有返回值,甚至在函数调用过程中,可以产生副作用
函数的定义:
return_type function_name(parameters){//parameters参数列表
statements //函数体
}
1.参数是值传递的(重要):C语言中只有值传递
值传递:在被调函数中是不能修改主调函数(main)中传过来参数原来的值。----->如何改变主调函数传过来的参数:指针
①一维数组作为参数传递(退化为指针)
int sum_array(int arr[]){}, int arr[]形式上是数组,但它其实是一个指针
缺点:丢失类型信息,丢失数组长度
优点:可以避免复制数据(节省内存空间)
可以修改原数组的值
函数调用会更加灵活
②二维数组作为参数传递
不能省略列的信息
传递一个列数不用定的数组:二维数组,int*arr[6];
2.返回值
返回值类型不能是数组类型
3.程序如何终止
操作系统会调用main函数:程序的开始
main将状态码返回给操作系统:程序的结束
如何不main函数中终止程序:exit
<stdlib.h>
void exit(int exit_code);
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
void foo(){
printf("...");
exit(EXIT_FAILURE);
}
4.局部变量与外部变量
局部变量:定义在函数里面的变量 块作用域:{ }从变量定义开始,到对应的块的末尾.
外部变量(全局变量):定义在函数外面的变量 文件作用:从变量定义开始,到文件的末尾
void foo(){}
int n = 10;
void bar(){}
存储期限(***重要)
自动存储期限:
存放在栈里面的数据,具有自动存储期限。变量的生命周期随着栈帧的入栈而开始,随着栈帧出栈而结束。
静态存储期限:
拥有永久的存储单元,在程序整个执行期间都存在。
stack:具有自动存储期限
Heap:程序员自己管理
Code&Data:静态存储期限
外部变量:静态存储期限
局部变量:默认是自动存储期限,但是可以通过static关键字设为静态存储期限
5.递归(***重要)
recursion = re(重复) + cur(走) + sion(名词后缀) ,走重复的路
1.Fibnacci数列: 0,1,1,2,3,5,8,13..
Fn = Fn-1 +Fn-2 ,if n >= 2
1 , n = 1
0 , n = 0
若用递归的方法做,会进行大量的重复计算
如何避免重复计算?
①顺序求解子问题,避免出现重复计算问题,动态规划
数组:空间复杂度o(n)
long long fib2(int n){
if(n == 0) return 0;
if(n == 1) return 1;
//n >= 2
int a = 0, b = 1;
for(int i = 2; i <= n; i++){
int tmp = a + b;
a = b;
b = tmp;
}
return b;
}
②可不可以用通项公式求解
不可以,浮点数不精确
③更快的求解方式(线性代数)
2.汉诺塔
3个杆子
Base Case .n = 1时会移动吗
假设会移动n-1个盘子,如何移动n个盘子?
递归公式:
先将上面的n-1个盘子移动到中间杆子上
将最大的盘子移动到目标杆子上
将n-1个盘子从中间杆子上移动到目标杆子上
移动的次数:
S(n) = 1,n=1
2S(n-1) + 1,n>=2
S(n) = 2n -1
void hanoi(int n,char start,char middle,char target){
//base case
if(n == 1){
printf("%c -> %c\n",start, target);
return;
}
//递归公式
hanoi(n - 1, start, target, middle);
//把最大盘子从start移动到target上
printf("%c -> %c\n",start, target);
hanoi(n - 1,middle,start,target);
}
3.约瑟夫环
Joseph(n,m) = (Joseph(n - 1,m) + m )% n //递推公式
总结:
1.什么情况下考虑使用递归?
一个大问题可以分解成若干个小问题,且小问题的求解方式和大问题一致(递)
可以将这些小问题的解合并成大问题的解(归)
2.使用递归时需要注意问题
边界条件:避免stackoverflow问题,重复计算
3.如何写递归
①边界条件
②递推公式
6.指针基础(***重要)
计算机的最小寻址单位:字节
变量的地址:变量第一个字节的地址
指针:简单来说,指针就是地址
指针变量:存放地址的变量,有时候也把指针变量称为指针
1.指针变量只是存放变量的首地址,那怎么通过指针访问指针指向的对象?
声明指针式,需要指名指针的基础类型(指针指向对象的类型)
int *p,arr[10];
注意:变量名为p,不是*p;变量的类型为int * ,而不是int;arr的类型为int[10]
**2.两个基本操作:& 和 ***
取地址运算符:& int i =1,int* p = &i;
解引用运算符:* (通过指针访问指针指向的变量),**p相当于i的别名,修改 p 相当于修改i
i:直接访问(访问一次内存)
*p:间接访问(访问两次内存)
3.野指针问题
野指针:未初始化的指针或者是指向未知区域的指针
int *p;
int *q = 0x7F;上述都是野指针
注意:对野指针进行解引用运算,会导致未定义行为
4.指针变量的赋值
①使用取地址运算符 p = &i;
②通过另外一个指针变量q赋值,p = q;
注意事项:p = q 和 *p = *q的区别, *p = *q,直接把q指向的变量的值赋值给了p指向的变量的值,变量的值发生了改变
5.指针作为参数
好处:在函数出栈时,可以在被调函数中,通过指针修改在栈底的主调函数的参数的值
7.指针和数组的关系
1.指针的算数运算
当指针指向数组的元素时,可以通过指针的算数运算+、-访问数组的其它元素
①指针加上一个整数
②指针减去一个整数
③两个指针相减(两个指针指向同一个数组的元素)
注意:以基础类型(int)所占的字节数为基本单位
2.用指针处理数组
&arr[10]只会计算地址,并不会访问arr[10],因此不会发生数组越界现象
*和++(--)的组合:
*p++ , *(p++) 表达式的值为 *p,副作用p自增
(*p)++ 表达式的值为 *p,副作用 *p自增
*++p , *(++p) 表达式的值为 *(p+1),副作用p自增
++*p,++( *p) 表达式的值为 *p+1,副作用 *p自增
3.数组名可以作为指向索引为0的元素的指针
4.指针也可作为数组名使用(可以对指针使用[]运算符)
p[i ] = *(p+i)
总结:
①可以利用指针处理数组(指针的算术运算)
②数组名可以作为指向索引为0元素的指针
③指针也可以作为数组名来使用
8.字符串
1.字符串字面值
在编译时,编译器把两个相邻的字符串字面值,合并成一个字符串字面值
相邻:当两个字符串字面值仅以空白字符分割时
字符串字面值是如何存储的:"abc"
a | b | c | \0 |
---|
""-> \0
printf("Hello World.\n") ,"Hello World.\n"传递的其实是一个指向存字符串字面值数组的字符指针,char*
2.字符串变量
C语言没有专门的字符串类型,C语言的字符串依赖字符数组存在
char name[10] = "Allen"; "Allen"不是字符串字面值,本质上是一个初始化式{'A','l','l','e','n'}的简化形式
字符串的初始化:
char name[5] = "Allen";
char name[10] = "Allen";
char name[] = "Allen"; (推荐做法)
字符串的初始化和字符指针的初始化
char name[] = "Allen";数组的初始化式 ,可以进行修改操作,name[0] = 'a';
char *name = "Allen";"Allen"字符串字面量 ,是常量,不可以进行修改操作
3.字符串读写
写:printf + %s
puts
puts(name) 等价于 printf("%s\n",name)
读:scanf + %s (不推荐)
匹配规则:会跳过前置的空白字符存入数组,直到遇到空白字符,最后会添加'\0'.
注意:永远不会包含空白字符;不会检查数组是否越界
gets:
不会跳过前置的空白字符,读取字符到数组中,直到遇到换行符为止,并且会把换行符替换成空字符
缺陷:不会检查数组是否越界
解决办法:创建一个读字符串的函数:
char* read_line(char* str, size_t n) {
char* tmp = str;
//至多读取n个字符
while ((*tmp = getchar()) != '\n' && n-- > 0) { tmp++; }
//若读取了n个字符也没有读到换行符,则将str指向的内存放入空字符,并返回
if (n == -1 && *tmp != '\n') {
*str = '\0';
while (getchar() != '\0');
}
//将换行符替换为空字符
else *tmp = '\0';
return str;
}
4.字符串的库函数
①size_t strlen (const char*s); strlen(s1)//计算字符串的长度,不包括'\0字符'(字符串是以'\0'结尾的)
const指名在strlen中不会修改s指向的内容,传入参数
const(指针指向对象不能修改)char(基类型)*(指针)s; // 指针常量
char* const s(s不能改变); //常量指针
*②int strcmp(const char*s1,const char s2)//string compare //strcmp(s1,s2)
按字典顺序比较s1和s2,"abc" < "cba"(第一个比较完a<c,后面就不用比较了) "abc" < "abd"(前两个ab比较相等,故比较第三个c和d)
若s1 > s2,则返回正整.
若s1 = s2,则返回零.
若s1 < s2,则返回负数.
*③char strcpy(char s1, const char s2) //strcpy(s1,s2)
char *s1:表明通常会在函数中修改s1指向的内容,传入传出参数
作用:把s2指向的字符串,复制到s1指向的数组中,但是不会检查s1数组是否越界
有另一个函数定义<string.h>中的可以控制复制的写入s1中字符的个数
char* strncpy(char *s1, const char *s2,size_t count); // strncpy(s1,s2,4)
④char strcat (char dest, const char*src)** //strcat(dest,src)
将字符串src追加到dest的末尾,并且返回dest(不会检查数组是否越界)
控制追加的字符数:
char *strncat(char *dest, const char *src, size_t count);//strncat(s1,s2,4)
strncat总会在后面添加空字符
strncat(s1,s2,sizeof(s1)-sizeof(s2)-1)//-1是为了预留空间给空字符
5.惯用法
//自己编写strlen函数
size_t my_strlen(const char* s){
char* p = s;
while (*p){
p++;
}
return p - s;
}
//自己编写strcat函数
char* my_strcat(char* s1,const char* s2){
//搜索s1的末尾
char* p = s1;
while (*p){
p++;
}
//p指向空字符
while(*s2 != '\0'){
*p = *s2;
p++;
s2++;
}
*p = '\0';
return s1;
//另一种复制字符串包括空字符方法
while(*p++ = *s2++)
;
return s1;
}
9.字符串数组
1.如何表示字符串数组
//二维数组
char planets[][8] = {"Mecury","Venus","Earth",...};
若对该二维数组排序,会复制字符串
缺陷:浪费内存,不灵活
//字符指针数组
char *planets[][8] = {"Mecury","Venus","Earth",...};
若对该二维数组排序,就只会交换指针
会有一个一维大小为8的存指针的数组,如a[0],就存的指向"Mecury"的指针,因此会非常灵活
10.命令行参数
程序的开始:操作系统调用main 操作系统-->(命令行参数)-->程序
程序的结束:main返回(exit)
项目->属性->调试->输入命令参数
11.结构体(***重要)
C语言:结构体 <--->类(数据,方法)
只能定义数据
如何表示一个学生对象:
属性:学号、姓名、性别、语文成绩,数学成绩,英语成绩
typedef struct student_s{
int number;
char name[26];
bool gender;
int chinese;
int math;
int english;
}student,*pstudent;
内存布局:
number(4字节)name(26)gender(1)填充padding(1)chinese(4)math(4)english(4)
1.结构体对象的初始化(和数组类似)
struct student_s s1 = {1,"liulang",falise,100,100,100};未初始化的属性(成员)会被赋值为0
2.基本操作
获取成员 s1.name
赋值 s1 = s2
当结构体作为参数或者是返回值时,会拷贝整个结构体的数据,为了避免拷贝数据,我们会传递指向结构体的指针,student_s *pstudent
为了方便使用指向结构体的指针,C语言提供s->name 等价于 (*s).name
typedef给结构体起别名
12.枚举
在程序中,有一些变量只能取一些离散的值
缺陷:如果离散值太多,宏定义也会很多;编译器不能给出一些友好的提示
为此,C语言提供枚举类型:
enum suit{SPADE, HEART, CLUB, DIAMOND};
//其值从0开始分别为;SPADE 0 ,HEART 1 ,CLUB 2 ,DIAMOND 3
枚举的值,本质上是一些整数;除了上述操作外夜可以指定枚举类型的值:
enum suit{
SPADE, //0
HEART = 7 //7
CLUB, //8
DIAMOND = 16 //16
};
还可以给枚举类型起别名:
enum suit{
SPADE, //0
HEART = 7 //7
CLUB, //8
DIAMOND = 16 //16
}SUIT;
13.指针的高级应用
动态内存分配(***重要)
前置:头文件:< stdlib.h>
①void* malloc (size_t size);
给void型的某个变量分配size个字节的空间
②void* calloc (size_t num, size_t size)
分配 num*size 个字节的空间,并且会清零
③void* realloc(void *ptr,size_t new_size);
当ptr : NULL时 ,等价于malloc(new_size)
ptr应该时以前通过malloc,calloc,realloc返回的指针
把以前申请的内容空间调整为new_size个字节
realloc:扩容、缩容 扩容:以前的数据不变,增加的空间未进行初始化 缩容:截断
注意事项:
如果分配成功返回分配内存空间的地址,如果未分配成功,返回null(空指针)
void *:通用指针,可以指向任意类型,可以转换成任意其它类型的指针.如(student *)malloc(sizeof(student))
空指针:不指向任何对象的指针(用宏null代表空指针,其值为0)
释放内存空间:
如果申请的内存空间没有释放,会造成内存泄漏现象
p = malloc(..);
q = malloc(..);
p = q;//p申请的内存空间无法再访问,就是"垃圾"
内存泄漏:如果程序中存在垃圾,这种现象就叫内存泄漏
长时间运行,内存泄漏会导致OOM(Out of Memory)现象(堆内存空间地址用完了)
如何避免内存泄漏?及时释放无用的内存空间
void free(void *ptr)//ptr:先前调用malloc,calloc,realloc返回的指针
使用free虽然可以避免内存泄漏,但也引入了一个新问题:悬空指针
p = malloc(...);
q = p;
free(p);
strcpy(q,"abc");//q指向的内存空间其实已经释放掉了,q悬空
悬空指针非常难以发现,释放指针p,会导致所有指向相同内存的指针都悬空.
二级指针
一级指针存放的是指向之前(用malloc分配的内存里的一段内容)的起始地址,而且一级指针也要占空间,二级指针就是存放的这片空间的起始地址;
①一级指针可以在函数内部修改实参指针指向的内容,(可以将变量通过参数带入函数内部,但没法将内部变量带出函数)
②二级指针作为函数参数,可以在函数外部定义一个一级指针p,在函数内给指针赋值,函数结束后对p生效,(二级指针不但可以将变量通过参数函数内部,夜可以将函数内部变量带出到函数外部)
③当需要改变数组所指向的数据的时候,需要用二级指针传参数
④可以通过二级指针遍历指针数组的内容
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char* pArray[] = { "apple", "nokia", "google", "samsung", "huawei", NULL };
// 通常把指针数组的最后一个元素设置为NULL
// 这样就可以通过while循环直接遍历指针数组
// 而不需要知道指针数组内部元素的长度
char** p = pArray;
// 指针数组可以直接赋值给二级指针
// pArray是指向一个指针数组首元素的char*类型的指针
// 对一个 char*类型的指针再引用就是char**类型
// 通过二级指针遍历指针数组中的内容
int i = 0;
while (*p)
{
// 两种方法一致
printf("%s ", *p++);
printf("%s\n", pArray[i]);
i++;
}
system("pause");
return 0;
}
改变指针的值,而不是改变指针指向对象
Node* list = NULL;
void add_to_list(Node** ptr_list,int val){
Node* newNode = (Node*)malloc(sizeof(Node));
if(newNode == NULL){
printf("Error: malloc failed in add_to_list.\n");
exit(1);
}
newNode->val = val;
newNode->next = *ptr_list;
*ptr_list = newNode;
}
指针函数
本质是一个函数,不过其返回值是一个指针
int * func_sum(int n)
{
if (n < 0)
{
printf("error:n must be > 0\n");
exit(-1);
}
static int sum = 0;
int *p = ∑
for (int i = 0; i < n; i++)
{
sum += i;
}
return p;
}
上例就是一个指针函数的例子,其中,int * func_sum(int n)
就是一个指针函数, 其功能十分简单,是根据传入的参数n
,来计算从0到n的所有自然数的和,其结果通过指针的形式返回给调用方
函数指针
本质上是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针,函数指针就是指向代码段中函数入口地址的指针。
函数指针的声明:
ret (*p)(args,...);
函数指针的初始化:
函数指针名 = 函数名
示例:
#include <stdio.h>
int max(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
int (*p)(int, int); //函数指针的定义
//int (*p)(); //函数指针的另一种定义方式,不过不建议使用
//int (*p)(int a, int b); //也可以使用这种方式定义函数指针
p = max; //函数指针初始化
int ret = p(10, 15); //函数指针的调用
//int ret = (*max)(10,15);
//int ret = (*p)(10,15);
//以上两种写法与第一种写法是等价的,不过建议使用第一种方式
printf("max = %d \n", ret);
return 0;
}
为什么要使用函数指针?
在大型项目中有奇效,实现数组的排序,若不管内部实现,除了函数名不一样之外,返回值,包括函数入参都是相同的,这时候若要调用不同的排序方法,就可以使用指针函数来实现,我们只需要修改函数指针初始化的地方,而不需要区修改每个调用的地方.
函数指针的一个典型应用就是回调函数
回调函数就是一个通过指针函数调用的函数。其将函数指针作为一个参数,传递给另一个函数。回调函数并不是由实现方直接调用,而是在特定的事件或条件发生时由另外一方来调用的。同样我们来看一个回调函数的例子:
#include<stdio.h>
#include<stdlib.h>
//函数功能:实现累加求和
int func_sum(int n)
{
int sum = 0;
if (n < 0)
{
printf("n must be > 0\n");
exit(-1);
}
for (int i = 0; i < n; i++)
{
sum += i;
}
return sum;
}
//这个函数是回调函数,其中第二个参数为一个函数指针,通过该函数指针来调用求和函数,并把结果返回给主调函数
int callback(int n, int (*p)(int))
{
return p(n);
}
int main(void)
{
int n = 0;
printf("please input number:");
scanf("%d", &n);
printf("the sum from 0 to %d is %d\n", n, callback(n, func_sum)); //此处直接调用回调函数,而不是直接调用func_sum函数
return 0;
}
在这个程序中,回调函数callback
无需关心func_sum
是怎么实现的,只需要去调用即可。
这样的好处就是,如果以后对求和函数有优化,比如新写了个func_sum2
函数的实现,我们只需要在调用回调函数的地方将函数指针指向func_sum2
即可,而无需去修改callback
函数内部。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架