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 = &sum;
    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函数内部。

posted @   饮马江湖风萧萧  阅读(334)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示