C语言学习记录

C语言

C语言

数据的表示

​ 包括二进制、八进制、十进制、十六进制

  • ​ 数值型数据:能够进行算术运算并且得到明确结果的数据
  • ​ 非数值型数据:不是数值型数据的其他数据

基数与权的关系

​ 基数:该进制中基本数码的个数

​ 十进制:0~9十个,二进制:0、1两个

​ 数位的权:以基数为底,数字所在位置的序号为指数的整数次幂

​ 二进制:0 1

​ 八进制:0 1 2 3 4 5 6 7

​ 每一位八进制数都可以用三位二进制数表示

​ 0—000 1—001

​ 2—010 3—011

​ 4—100 5—101

​ 6—110 7—111

​ 十进制:0 1 2 3 4 5 6 7 8 9

​ 十六进制:0 1 2 3 4 5 6 7 8 9 A B C D E F

​ 每一位十六进制数都可以用四位二进制数表示

​ 0—0000 1—0001

​ 2—0010 3—0011

​ 4—0100 5—0101

​ 6—0110 7—0111

​ 8—1000 9—1001

​ A—1010 B—1011

​ C—1100 D—1101

​ E—1110 F—1111

进制的表示

进制前缀后缀
二进制0bb/B
八进制0O
十进制d/D
十六进制0xh/H

进制之间的相互转换

  1. 转二进制

    • 八进制转二进制:每一位八进制数都可以用三位二进制数表示

      从右到左(低位到高位)每一个八进制数转换为三位二进制数

      0345 -> 011 100 101

    • 十进制转二进制:除二倒取余

      98 -> 100010

    • 十六进制转二进制:每一位十六进制数都可以用四位二进制数表示

      从右到左(低位到高位)每一个十六进制数转换为四位二进制数

      0x23c -> 0010 0011 1100

  2. 转八进制

  • 二进制转八进制:

    从右到左(低位到高位)每三位二进制数转换为一位八进制数,高位不足补0

    0b11 010 110 -> 0326

  • 十进制转八进制:除八倒取余

    217 -> 0331

  • 十六进制转八进制:先转为二进制再转为八进制

  1. 转十进制:
  • 二进制转十进制

0b1101 = 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3 = 1 + 0 + 4 + 8 = 13

  • 八进制转十进制

0744 = 4*8^0 + 4*8^1 + 7*8^2 = 4 + 32 + 448 = 484

  • 十六进制转十进制

0x1cf = 15*16^0 + 12*16^1 + 1*16^2 = 15 + 192 + 256 = 463

​ 4. 转十六进制

  • 二进制转十六进制:

    从右到左(低位到高位)每四位二进制数转换为一位十六进制数,高位不足补0

    0b101 1101 0010 -> 0x5D2

  • 八进制转十六进制:先转为二进制再转为十六进制

    十进制转十六进制:除十六倒取余

    415 -> 0x19F

非数值型数据中的字符数据

ASCII—美国信息交换标准代码

‘ ’ — 32

‘0’ — 48

‘A’ — 65

‘a’ — 97

  • UTF-8编码表示一个中文需要三个字节

C语言发展历史:

​ 1972年诞生,1973年发布

​ 在1989年发布了C89标准

​ ISO在1990年发布了C90标准

​ 在1999年发布了C99标准

​ 在2011年发布了C11标准

C语言是编译型语言,不能字节运行,需要编译器编译成计算机能识别执行的二进制程序

编译器支持对应标准,默认C90.

-std=cxx 90 99 11

学习C基础固定的词汇和语法格式

32个关键字和9个控制语句

img

C语言代码的格式

  1. 针对文件 xx.c C语言源码文件后缀是 .c

  2. 代码基本结构

    //C语言注释
    
    //是注释一行,//后边的内容会注释掉
    /*是注释一段内容,**之间的内容被注释掉*/
    
    /*
    	#表示预处理部分,在编译时就会被处理
    	include 表示头文件声明
    	<stdio.h>是具体的头文件
    */
    
    #include <stdio.h>
    //main:是程序的入口,有且只有一个
    int main(void)
    {				//函数模块的开始
        /*printf是函数功能语句,左右是将制定内容在终端输出
        ""包含的内容会被输出,\n是换行标志*/
        printf("chengxing");
        return 0;
    }				//函数模块的结束
    

GCC编译器的基本使用

  1. 生成默认的可执行程序,生成a.out

    gcc xx.c

  2. 生成指定的可执行程序名称

    gcc xx.c -o 指定可执行程序名

运行程序:

./a.out

./可执行程序名

C语言运行异常:

编译异常,通常是语法错误

  • warring:警告,有可能生成可执行程序,不保证结果正确

  • error:错误,绝对不会生成可执行文件

  • 发生编译异常,从第一个警告或错误开始处理;执行异常, 通常是代码逻辑错误。

计算机单位

​ CPU数据处理最小单位1bit(比特)

​ 内存最小存储单位1Byte

​ 1Byte = 8bit

​ 1KB = 1024B

​ 1MB = 1024KB

​ 1GB = 1024MB

​ 1TB = 1024GB

数据类型

​ 在 C 语言中,数据类型指的是用于声明不同类型的变量函数的一个广泛的系统。变量的类型决定了变量存储占用的空间以及存储模式

graphic

  • 在C标准定义中,只规定了数据存储习惯。没有规定数据类型要占用的内存大小,要根据具体使用的平台决定。

数据分为有符号数和无符号数

  • 有符号数:使用signed表示,有正数和负数区别,通常默认不写

  • 无符号数:使用unsigned表示,只有正数,最小为0

针对于signed有符号数,数据的最高位表示为符号位,其他位表示数据

  • 正数:最高位为0

  • 负数:最高位为1

  • 针对于unsigned无符号数,所有二进制位都表示数据

Ubuntu 64位:

整型存储空间大小(字节)unsigned值域范围signed值域范围
短整型short20~2^16-1-215~215-1
整型int40~2^32-1-231~231-1
长整型long80~2^64-1-263~263-1
浮点型存储空间大小(字节)精度
单精度float4小数点后6位
双精度double8小数点后15位
字符型存储空间大小(字节)unsigned值域范围signed值域范围
字符型char10~2^8-1-27~27-1
  • 计算机中存储浮点数据不是准确存储的,是近似存储!!!

    2.3

    ​ 2.3000 0004 3694 63

    ​ 2.3000 0000 0000 0001 2113 6778

float数据与0比较

-0.000001 < float 并且 float < 0.000001

char 类型

unsigned char: 0000 0000 ~ 1111 1111 0~255

signed char: 1000 0000 ~ 0111 1111 -128~127

对于正数 5 :

​ unsigned char : 0000 0101

​ signed char : 0 000 0101

计算机存储负数,是存储负数的补码

补码是原码取反得到反码,然后+1

对于正数原码、反码、补码都是原码本身

对于负数 -5

​ 原码:1000 0101

​ 反码:1111 1010 符号位不变,数据为取反

​ 补码:1111 1011 符号位不变,数据+1

​ 在计算机中,-5存储的二进制数据位 1111 1011

Signed原码(+1)补码(-1)unsigned原码补码
-11000 00011111 111110000 00010000 0001
-21000 00101111 111020000 00100000 0010
-31000 00111111 110130000 00110000 0011
-1261111 11101000 00101260111 11100111 1110
-1271111 11111000 00011270111 11110111 1111
-128----- ----1000 00001281000 00001000 0000
1291000 00011000 0001
1301000 00101000 0010
2531111 11011111 1101
2541111 11101111 1110
2551111 11111111 1111
  • -128是没有原码、反码,直接规定了补码是1000 0000

对于signed char:

0~127 正数部分

-128~-1 负数部分

#include <stdio.h>
int main(int argc, char *argv[])
{
	char a = 0b10000000;
    printf("%d\n",a);
    printf("%d\n",0b10000000);
    printf("%d\n",(char)0b10000000);
    
    return 0;
}
  • 计算机操作的是二进制的补码

常量和变量

常量:在程序运行期间其数值不会发生任何变化的数据

整型常量:

​ 二进制数、八进制数、十进制数、十六进制数

可以使用后缀表示数据类型,编译器处理常量默认使用signed int存储

​ 100U U表示无符号类型 unsigned int

​ 500L L表示long类型 signed long

浮点常量:

​ 2.3 3.14

​ 4f f表示浮点数据

字符常量:

​ ‘A’ ‘c’ ‘+’ ‘ ’

​ 特点:字符常量使用 ‘ ’ 括起来,只能由一个字符组成

字符串常量

​ “hello!” “110120130” “石油路” “张三” “渝州路1001” “A”

​ 特点:字符串常量使用 “” 括起来,字符串的末尾由 ’\0’ 作为结束标志

标准格式化输出函数----printf的使用

  1. 向终端输出指定内容字符串

    printf(“要输出的内容\n”);

  2. 格式化输出

    printf(“格式化输出的内容\n”, 值1, 值2, 值3, …, 值n);

​ 格式化字符

%d signed int 十进制整型

​ %hd signed short

%ld signed long

​ %u unsigned int

%lu unsigned long

​ %f float

​ %lf double

​ %c char 字符输出

​ %s 字符串输出

​ %o 八进制输出

​ %x 十六进制输出

​ %#x 带0x前缀

int main()
{
	printf("%d\n",(signed int)0b10011100);
	printf("%u\n",(unsigned int)0b10011100);
	printf("%hd\n",(unsigned short)0b10011100);
	printf("%ld\n",(signed long)0b10011100);
	printf("%lu\n",(unsigned long)0b10011100);
	printf("%c\n",(char)0b10011100);
	//printf("%f\n",(signed float)0b10011100);
	//printf("%lf\n",(unsigned float)0b10011100);
	//printf("%s\n",(signed double)0b10011100);
	printf("%x\n",0b10011100);
	printf("%#x\n",0b10011100);
	printf("%o\n",0b10011100);
}

sizeof运算符:求取数据类型或者是变量所占内存空间的大小,单位是字节

 printf(%lu\n”,  sizeof(int)  ); 					//使用%lu

gcc -Wall xx.c -Wall严格的语法检查

变量

​ 变量是一片存储空间的别名,定义一个变量就相当于创建了一个对象,并且给这个对象分配了一片内存空间,这片空间的名字就是变量名

定义:

存储类型 数据类型 变量名;

存储类型:决定变量的存储模型,存在什么地方,默认存储类—自动存储类auto,当没有存储类型时,默认为auto。

  • 数据类型:变量的数据类型,short、int、long、float、double、char
  • 变量名:开辟存储空间名字,准许变量命名规则

命名规则:

  1. 不能是数字开头,需要使用字符或_开头

  2. 不能和关键字重名

  3. 严格区分大小写

  4. 见名知意,变量具有的有具体含义

  5. 多个单词不能有空格

定义形式:

  1. 只定义
int a;           	//auto signed int a;

unsigned char c;  	// auto unsigned char c;
  1. 定义并初始化,在定义的同时赋值
 int a = 10;      	//= 为赋值运算符,见右边表达式赋值给左边操作数
  1. 先定义,后赋值
int a;

a = 20;
  1. 一次定义多个变量
int a, b;     		//同时定义两个变量a, b 都是int类型

int a = 10, b;

int a = 10, b = 20;

局部变量与全局变量

  • 局部变量:在函数体内部定义的变量就是局部变量。如果没有被初始化其值为随机值,存储类为auto时,存放在堆区,系统自动开辟自动释放
  • 全局变量:在函数体外部定义的变量就是全局变量。如果没有被初始化其值是0,存放在全局区程序运行之初初始化操作。

生命周期与作用域

  1. 局部变量
  • 生命周期:从定义开始活着,到模块的结束就死去
  • 作用域:在模块内
  1. 全局变量
  • 生命周期:从程序开始活着,到程序的结束死去
  • 作用域:整个程序

类型转换

  1. 隐式类型转换(系统进行的类型转换)

graphic

规则:

  • 低字节向高字节转换

  • 有符号数向无符号数转换,但是不包括char与short

  • float类型势必会向double转换,char和short类型势必会向int类型转换

  • 强制类型转换符是一种不安全的转换,低字节数在向高字节数转换数转换时,不会丢失精度,但是高字节数向低字节数转换时,会丢失精度;

  1. 强制类型转换

​ 由用户进行的,将一种数据类型强制转换为另外一种数据类型

​ 语法:

(目标数据类型)变量/表达式/值;

int main(void)
{
	printf("%d\n", (int)1.1 + (int)2.2 + (int)3.3);			//浮点数转换为整型数据小数部分被舍弃,分别把数据转换为整型再计算
	printf("%d\n", (int)(1.1 + 2.2 + 3.3));					//浮点数据转换为整型数据小数部分被舍弃,先把浮点数据计算和,和再转换为整型
}
  • 总结
  1. 无论是隐式类型转换还是强制类型转换,本质上都是一种强制的转换

  2. 发生的转换是在运算过程中,不会对数据类型和数据值产生影响,转换是临时的。

运算符

算术运算符—7个

+(加)、-(减)、*(乘)、/(除)、%(取余/模)、++(自加)、--(自减)

  • 注意:

* / 的优先级 高于 + -,使用()

% 不能对浮点数进行,/ 得到的结果是整数

两个整数整除得到的是整数,有浮点数据,得到浮点数

  1. ++:

++在前:先自加,后赋值

++在后:先赋值,后自加

  1. –:

–在前:先自减,后赋值

–在后:先赋值,后自减

如果 ++、-- 单独成一行,在前和在后没有区别

关系运算符—6个

>(大于)、>=(大于等于)、<(小于)、<=(小于等于)、==(等于)、!= (不等于)

关系运算符通常用作表达式结果比较,比较的结果是逻辑结果,只有成立和不成立,真和假,false 和 true (零和非零)。

/*
if 表示判断
if (条件表达式)
{
	成立执行的内容;
}

if 表示选择
if (条件表达式)
{
	成立执行的内容;
} else {
	不成立执行的内容;
}
*/

逻辑运算符

&&(逻辑与)、||(逻辑或)、!(逻辑非)

​ 逻辑运算符用作连接符号两边的关系式,判断两边关系式的共同结果,结果是逻辑值,只有成立和不成立,真和假,false和 true (零和非零)。

  1. &&(逻辑与):

1 && 1 :1

1 && 0 :0

0 && 1 :0

0 && 0 :0

使用&&连接左右两边表达式,只要有一个表达式的逻辑结果是假的,整个表达式就不成立。只有两边的表达式都为真,整个表达式才成立

  1. ||(逻辑或):

1 || 1 :1

1 || 0 :1

0 || 1 :1

0 || 0 :0

使用||连接左右两边表达式,只要有一个表达式的逻辑结果是真的,整个表达式就成立。只有两边的表达式都为假,整个表达式才不成立

  1. !(逻辑非):

!1 :0

!0 :1

条件表达式不成立取 ! 会变为成立;条件表达式成立取 ! 会变为不成立。

位运算符—6个

&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)

​ 位运算符只要是对二进制位操作的。

操作有符号负数,是操作补码形式!!!

  1. &(按位与):

11010101

  &  10011001
  
  ​	 10010001

两个数进行按位与操作,是将两个数的二进制依次相与,只有两个二进制位都是1,结果才为1,只要有一个是0,结果就为0。(与0得0)

char a = 13, b = 7;

a & b = 5

​ 0000 1101

& 0000 0111

​ 0000 0101

char a = -13, b = -7;

a & b = -15;

​ 1111 0011 -13补码

& 1111 1001 -7补码

​ 1111 0001 – 某个数的补码

​ 1000 1111 -15

  1. |(按位或):

​ 11010101

| 10011001

​ 11011101

​ 两个数进行按位或操作,是将两个数的二进制依次相或,只要两个二进制位有一个是1,结果就为1(或1得1),只有两个都是0,结果才为0。

​ char a = 13, b = 7;

​ a | b = 15

​ 0000 1101

& 0000 0111

​ 0000 1111

​ char a = -13, b = -7;

​ a | b = -5

​ 1111 0011 -13补码

& 1111 1001 -7补码

​ 1111 1011 —某个数的补码

​ 1000 0101 -5

  1. ^(按位异或):

​ 11010101

^ 10011001

​ 01001100

​ 两个数进行按位异或操作,是将两个数的二进制依次相异或,只要两个二进制位相同,结果就为0,两个二进制不同,结果就为1。(相同为0,不同为1)

​ char a = 110, b = 33;

​ a ^ b = 79

​ 0110 1110

^ 0010 0001

​ 0100 1111

​ char a = -110, b = -33;

​ a ^ b = 77

​ 1001 0010 – -110补码

^ 1101 1111 – -33补码

​ 0100 1101 77

  1. ~(按位取反):

~ 10101101

​ 01010010

  • 对一个数进行按位取反,是对该数的每个二进制位依次取反,1变0,0变1.

​ char a = 45;

​ a = ~a;

~ 0010 1101

​ 1101 0010 — 某个数的补码

​ 1010 1110 -46

<<(左移):

​ 0000 0111 << 3

​ 00111000

​ 左移是将一个数的二进制位整体向左边移动,左边溢出的部分舍弃,右边空缺的部分补0。

>>(右移):

​ 0000 0111 >> 4

​ 0000 0000

左移是将一个数的二进制位整体向右边移动,右边溢出的部分舍弃

  1. 对于无符号数据:左边高位补0

  2. 对于有符号数据:

  • 正数:左边高位补0
  • 负数:左边高位补1 数据移动之前就是负数

赋值复合运算符—10个

​ =(赋值运算符) a = 3 将右边的值或者表达式结果赋值给左边的操作数

​ += a += b <==>a = a+b

​ 将右边的值或者表达式与左边的操作数相加,结果重新赋值给左边的操作数

​ -= a -= b <==>a = a-b

​ 将右边的值或者表达式与左边的操作数相减(左边操作数减右边),结果重新赋值给左边的操作数

​ *= a = b <==>a = ab

​ 将右边的值或者表达式与左边的操作数相乘,结果重新赋值给左边的操作数

​ /= a /= b <==>a = a/b

​ 将右边的值或者表达式与左边的操作数相除(左边操作数除右边),结果重新赋值给左边的操作数

​ %= a %= b <==>a = a%b

​ 将右边的值或者表达式与左边的操作数相取余(左边操作数%右边),结果重新赋值给左边的操作数

​ &= a &= b <==>a = a&b

​ 将右边的值或者表达式与左边的操作数按位与,结果重新赋值给左边的操作数

​ |= a |= b <==>a = a|b

​ 将右边的值或者表达式与左边的操作数按位或,结果重新赋值给左边的操作数

​ ^= a ^= b <==>a = a^b

​ 将右边的值或者表达式与左边的操作数按位异或,结果重新赋值给左边的操作数

​ <<= a <<= n <==>a = a << n

​ 将左边操作数左移n位,将结果重新赋值给左边操作数

​ >>= a >>= n <==>a = a >> n

​ 将左边操作数右移n位,将结果重新赋值给左边操作数

特殊运算符

​ sizeof:求取数据类型或变量所占内存空间的大小,单位是字节

三目运算符:

​ 格式: ? :

表达式 ? 表达式成立执行的内容 : 表达式不成立执行的内容
执行流程:首先判断表达式的逻辑结果,如果是成立的直接执行 表达式成立执行的内容 ,然后把结果作为整个三目运算符的结果,结束运行;如果是不成立,就执行 表达式不成立执行的内容, 然后把结果作为整个三目运算符的结果,结束运行。

三目运算符支持嵌套使用

​ 表达式 ? (表达式 ? 表达式成立执行的内容 : 表达式不成立执行的内容) : (表达式 ? 表达式成立执行的内容 : 表达式不成立执行的内容)

标准格式化输入函数----scanf

​ scanf(“格式化内容”, 地址1, 地址2, …, 地址n );

​ &变量名 获得变量的地址

练习:从终端输入三个数,使用三目运算符比较三个数的大小,找出最大的数。

int a, b ,c;

printf("请输入三个数:");
scanf("%d%d%d", &a, &b, &c);

printf("a = %d, b = %d, c = %d\n", a, b, c);
//当一行写不下时,使用\作为分割,下一行的额内容会被拼接起来

a > b ? \
    (a > c ? printf("max = %d\n", a) : printf("max = %d\n", c)) : \
    (b > c ? printf("max = %d\n", b) : printf("max = %d\n", c));

& 取地址运算符

获得变量的地址

逗号运算符

​ 格式: ,

表达式1, 表达式2, 表达式3, … , 表达式n;

​ 逗号运算符是在表达式中决定最后的内容。

​ 有逗号运算符的表达式是从左往右依次结合的,会依次执行每一个表达式,并且把最后一个表达式的结果作为整个表达式的结果。

指针运算符

​ *****

格式: *变量名

运算符优先级

​ https://baike.baidu.com/item/%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7/4752611?fr=aladdin#3https://baike.baidu.com/item/运算符优先级/4752611?fr=aladdin#3

控制语句

​ 顺序、选择、循环

选择

​ if、switch

​ if语句

  1. 表选择
if (条件表达式)
{
	成立执行的内容;
}   
  • 遇到if语句,判断条件表达式,成立执行内容,不成立执行if语句下边的内容

  1. 二分枝选择
if (条件表达式)
{
	成立执行的内容;
}
else {
不成立执行的内容;
}
  • 遇到if语句,判断条件表达式,成立执行if语句的内容,不成立执行else语句的内容
  1. 多分枝选择
if (条件表达式1)
{
	表达式1成立执行的内容;
}
else if (条件表达式2)
{
	表达式2成立执行的内容;
}
else if (条件表达式3)
{
	表达式3成立执行的内容;
}else if (条件表达式n)
{
	表达式n成立执行的内容;
}

else {
	上边所有条件都不成立执行的内容;
}

首先判断条件表达式1是否成立,如果成立执行对应的内容,执行完结束整个if语句执行;如果条件不成立,会跳到下一个表达式判断,重复步骤,直到条件成立或者是遇到了else语句或者是if语句结束

switch case多分枝选择
switch (整型/字符型表达式)
{
	case 常量1:
           匹配成功执行的内容
           break;
    case 常量2:
           匹配成功执行的内容
           break;
    case 常量3:
           匹配成功执行的内容
           break;case 常量n:
           匹配成功执行的内容
           break;
    default:
       上边匹配都不成功执行的内容
} 
  • 整型/字符型表达式:需要数值型数据,必须是整型数据

  • 执行流程:switch的整型/字符型表达式依次取匹配case的常量(常量表达式),匹配成功就执行语句内容,然后通过break结束整个switch语句的执行,如果匹配不成功,会跳到下一个case继续匹配,一直到default语句或者是switch语句的结束

  • break:用作结束(跳出)switch语句的执行。匹配到一个case时,语句块内省略掉break时,会不匹配下边所有的case语句,直接执行里边的内容,直到遇到下一个break或是switch语句的结束。

switch语句嵌套使用

switch (a) {
    case 0:
    	switch (b) {
            case 1:
                printf("A\n");
                break;
            case 2:
                printf("B\n");
                break;
            default :
                printf("error\n");
        }
        break;
    case 1:
    	switch (b) {
            case 1:
                printf("C\n");
                break;
            case 2:
                printf("D\n");
                break;
            default :
                printf("error\n");
        }
        break;
    default :
        printf("error\n");
}

循环语句

while、do while、for

//while语句
while (条件表达式)
{
	循环体,成立执行的内容;
}

条件表达式需要的是逻辑结果,成立就执行循环体的内容,不成立就结束while循环。

通常情况下需要的是有限次的循环,所有会在while语句之前定义一个循环控制变量并且给一个合适的初始值,在while语句循环体内做控制变量的改变,逐渐趋向于循环结束。

//do while语句
do {
	循环体,成立执行的内容;
} while (条件表达式);

先执行一次循环体再判断条件是否成立,成立继续执行循环体,不成立结束do while语句。

  • while语句条件不成立时,至少执行0次

  • do while语句条件不成立时,至少执行1次

//for语句
for(初始表达式; 判断表达式; 控制表达式)
{
	循环体,判断表达式成立执行的内容;
}

初始表达式用作循环控制变量给初始值,最先被执行,且只会被执行一次。

判断表达式需要的是逻辑值,成立执行循环体,不成立退出for语句

控制表达式用作循环变量的控制,让循环逐渐趋向于结束

辅助控制语句

break

  1. switch中用作跳出case语句,结束switch语句的执行

  2. 在循环语句中,用作结束循环语句的执行(在多重循环中跳出当前循环)

continue

在循环语句中,用作跳过一次循环,continue语句下边的内容不会被执行,直接开始下一次循环

goto语句:不到万不得已不要使用!!!

做语句的跳转

语法: goto 标记;

标记: return 用作函数的返回

输入输出函数

函数:完成某个或系列特定功能的语句模块

学习函数使用了解函数原型、头文件、使用方法,函数原型告诉用户函数名称、形式参数列表、返回值数据类型。

标准格式化输出函数—printf

头文件:#include <stdio.h>

函数原型:int printf(const char *format, ...);

功能:用作标准格式化输出

形式参数列表:输出的格式化内容

返回值:成功,返回输出的字符个数,字节数;失败,返回EOF。

格式化字符:

​ %d signed int 十进制整型

​ %hd signed short

​ %ld signed long

​ %u unsigned int

​ %lu unsigned long

​ %f float

​ %lf double

​ %c char 字符输出

​ %s 字符串输出

​ %o 八进制输出

​ %x 十六进制输出

​ %#x 带0x前缀

​ %p 打印地址

附加格式说明符 在%后边添加

​ m 控制输出域宽,左边补空格。数据实际长度超过m,原样输出

​ .n 控制浮点数小数位数,第n+1位四舍五入

​ m.n 总数据长度为m其中小数长度为n,小数点也算一位长度

​ - 数据默认是右对齐,-用作左对齐

​ 0 空缺补0

​ + 整数前边显示+

转义序列:把原本字符用作特殊功能

​ \n 换行

​ \r 回车 通常使用\n\r

\t 水平制表符

​ ? ?

​ \” “

​ \ \

​ %% %

标准格式化输入函数—scanf

头文件:#include <stdio.h>

函数原型:int scanf(const char *format, ...);

功能:按照指定格式从键盘终端获得数据存入地址表中指定内存空间中

形式参数:地址表

返回值:成功,获得数据个数,失败,EOF。

  • 数值型数据和字符数据混合输入时,如果存在垃圾字符,可以使用%*c吃掉一个字符

数组和指针

​ 数组是具有一定顺序关系的若干变量的集合,组成数组的各个变量称为数组的元素;数组中各元素的数据类型要求相同,用数组名和下标确定

一维数组

​ 所谓一维数组是指只有一个下标的数组,它在计算机的内存中是连续存储的。

语法:

存储内型 数据类型 数组名[表达式]

数组名表示内存首地址,是地址常量;

编译时分配连续内存,内存字节数=数组维数*sizeof(元素数据类型)。

一维数组的引用:

数组必须先定义,后使用

只能逐个引用数组元素,不能一次应用整个数组

数组元素表示形式:数组名[下标]

一维数组的初始化

数组不初始化,其元素为随机数

对static数组元素不赋初值,系统会自动赋值以0值

只给部分数组元素赋初值

计算Fibonacci数列前10项并逆序给出结果

int main()
{
	int i, data[10];
    data[0] = data[1] = 1;
    for (i = 2; i < 10; i++)
        data[i] = data[i - 2] + data[i - 1];
    for (i = 9; i <= 0; i--)
        printf("%d\n",data[i]);
    
    return 0;
}

一维数组数组名相关操作

  1. 代表一维数组本身

  2. 代表首元素的首地址

int a[5];
printf("a:%p\n",a);				//代表首元素的首地址,即a[0]的地址(&a[0])
//数组名 +1,代表的是加一个数据类型的大小,a + 1即a[1]的地址(&a[1])
printf("a + 1:%p\n",a + 1);		
printf("a + 2:%p\n",a + 2);

printf("&a[0]:%p\n",&a[0]);
printf("&a[0] + 1:%p\n",&a[0] + 1);
printf("&a[1]:%p\n",&a[1]);
printf("&a[2]:%p\n",&a[2]);
printf("&a[2] - 1:%p\n",&a[2] - 1);

//&a表示把一堆数组看做一个整体,取地址得到的是一堆数组整体的首地址
printf("&a:%p\n",&a);
//移动一个一堆数组整体的大小
printf("&a + 1:%p\n",&a + 1);

二维数组

​ 看作是一维数组元素的集合

定义

存储类型 数据类型 二维数组变量名[行][列];

存储类型:二维数组中一维数组中元素的存储类型

数据类型:二维数组中一维数组中元素的数据类型

二维数组变量名:遵循变量的命名规则

行:多少个一维数组元素

列:一维数组中有多少个基本元素

示例:

  1. 只定义

    int a[2][3];

二维数组a存储了两个一维数组元素,每个一维数组元素都存储了3个基本元素,基本元素的数据类型是int

  1. 定义并初始化

int a[2][3] = {{1,2,3}, {4,5,6}};

// a[0][0] = 1 a[0][1] = 2 a[0][2] = 3

// a[1][0] = 4 a[1][1] = 5 a[1][2] = 6

  1. 先定义,后赋值

int a[2][3];

a[0][0] = 1; a[0][1] = 2;

a[0][2] = 3; a[1][0] = 4;

a[1][1] = 5; a[1][2] = 6;

#include <stdio.h>
#define M 3
#define N 3
int main(int argc, char *argv[])
{
    int i, j;
	int a[M][N] = {{1,2,3},{4,5,6},{7,8,9}};
    for (i = 0; i < M; ++i)				//第几个一维数组
    {
        for (j = 0; j < N; ++j)			//一维数组元素中第几个基本元素
        {
            printf("a[%d][%d] = %d\t", i, j, a[i][j]);
        }
        printf("\n");
    }
    return 0;
}
  • 赋值必须对二维数组的每个基本元素单独赋值

  • 二维数组的行列下标都是从0开始的

二维数组特殊定义方式

int a[2][3] = {{1,2,3}, {4,5,6}};				//一个{}表示一个一维数组元素内容
int a[2][3] = {1,2,3,4,5,6};					//将值依次赋值给一维数组的基本元素
int a[2][3] = {{1,2}, {4}};
//a[0][0] = 1  a[0][1] = 2   
// a[1][0] = 4
//其余全部为0
int a[2][3] = {1,2,4};

//a[0][0] = 1  a[0][1] = 2   a[0][2] = 4
//其余全部为0

int a[2][3] = {0};
局部二维数组未初始化,其值为随机值
全局二维数组未初始化,其值为0

二维数组可以省略 行 下标,省略时必须做初始化操作

//二维数组a有两个一维数组元素,行下标为2
int a[][3] = {{1,2,3},{4,5,6}};
int a[][3] = {1,2,3,4,5,6};
int a[][3] = {{1,2},{4}};
int a[][3] = {1,2,4,5};

二维数组名相关操作

  1. 代表了二维数组本身

  2. 代表了第一个一维数组元素的首地址,即第0行的首地址,被称为行地址

int a[3][3];

printf("a:%p\n", a);					//代表第一个一维数组元素的首地址,即a[0]的首地址&a[0]
printf("a + 1:%p\n", a + 1);			//二维数组名加1,移动一个一维数组元素的大小,即a[1]的首地址&a[1]

printf("&a[0]:%p\n", &a[0]);			//第一个一维数组元素的首地址
printf("&a[0] + 1:%p\n", &a[0] + 1);	//+1移动一个一维数组大小,即第二个一维数组元素的首地址
printf("&a[1]:%p\n", &a[1]);

printf("a[0]:%p\n", a[0]);				//第一个一维数组元素里边首元素的首地址
printf("a[0] + 1:%p\n", a[0] + 1);		//+1移动一个数据类型的大小,即第一个一维数组元素里边首元素的首地址
printf("a[1]:%p\n", a[1]);

printf("&a[0][0]:%p\n", &a[0][0]);
printf("&a[0][1]:%p\n", &a[0][1]);
printf("&a[1][0]:%p\n", &a[1][0]);

printf("&a:%p\n", &a);					//对二维数组名取地址,代表整个二维数组的首地址
printf("&a:%p\n", &a + 1);				//加1移动整个二维数组的大小

冒泡排序

相邻数据,两两比较,将大的(从小到大)或小的(从大到小)数据放后边交换位置

//冒泡排序
for (i = 0; i < N - 1; i++)				//排序使用的轮次
{
    for (j = 0; j , N - i - 1; j ++)	//在一轮中需要比较交换的次数
    {
        if (a[j] < a[j + 1])			//从大到小排序,小的放后边
        {
            temp = a[j];
            a[j] = a[j + 1];
            a[j + 1] = temp;
        }
    }
}

选择排序

在一轮中找到最大或最小的数,把它交换到合适的位置

//选择排序
int max;
for (i = 0; i < N - 1; i++)			//排序使用的轮次
{
	//j表示的是当前轮次中用作大小比较元素的下标,当前轮下标范围
    for (j = i, max = j; j < N - 1; J ++)
    {
        if (a[max] < a[j + 1])
        {
            max = j + 1;
        }
        temp = a[j];
        a[j] = a[j + 1];
        a[j + 1] = temp;
    }
}

指针

在内存中,每字节空间都被编号,这个编号被称为地址。在C语言中,将地址称为指针。

指针变量:存放内容为地址的变量(变量是对程序中数据存储空间的抽象)

在不加区分的情况下,将地址,指针,指针变量统统称为指针

#include <stdio.h>
int main(int argc, char *argv[])
{
	int a = 10;
	int *p = &a;
	printf("&a:%p\n", &a);
	printf("p:%p\n", p);
	printf("&p:%p\n", &p);
	
	printf("a = %d\n", *p);
	printf("a = %d\n", **p); 	//错误
	printf("a = %d\n", p->a); 	//错误
}
  • 地址在程序运行期间是地址常量!!!

  • 指针变量也是变量的一种,遵循变量的一些规则。

直接访问和间接访问

直接访问:定义一个变量,分配了一片内存空间,空间有两个属性,一个是变量的地址,一个是变量保存的值。直接访问是可以通过变量直接拿到变量的数据。

间接访问:需要通过其他内存空间获得要访问内存空间的地址,通过得到的内存空间地址访问对应空间,进而拿到真实的数据,这种访问方式就是间接访问。

一级指针变量

​ 一级指针变量保存的地址指向的空间存放的是真实数据。

定义:

存储类型 数据类型 *一级指针变量名;

存储类型:指针变量本身的存储类型

数据类型:指针的存储类型是指指针变量本身的存储类型

一级指针变量名:遵循变量命名规则

*:表示是一个一级指针变量声明标志

一级指针变量本身的类型: 数据类型 *

示例:

  1. 只定义
int *p;						//指针变量p需要存放int类型数据的地址
  1. 定义并初始化
int a = 10;
int *p = &a;             	//指针变量p保存了int类型变量a的地址量。
  1. 先定义,后赋值
int a = 10*p;				//int a = 10; int *p;
p = &a;
指针运算
指针变量运算

*指针运算符

​ 通过访问内存地址获得内存空间中的数据

​ *(指针变量):取得指针变量保存地址指向内存空间的真实数据

野指针:定义指针变量时,没有给出明确地址指向,局部指针变量保存随机地址,这样的指针变量被称为野指针。操作野指针是很危险的事情。

int \*q = NULL;     //通常在定义指针变量时,将指针变量指向为NULL防止称为野指针

NULL:空指针,是内存空间中一片特殊的内存区域,这片空间不允许做操作,非法操作会引发内存相关错误。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RIwUXkNm-1648799121431)(ipic%5Cclip_image006-1648471564335.jpg)]

NULL实际上是系统定义的一个宏,一般是0x00,让一个指针指向0号地址空间,系统不会将0号空间分配给任何进程使用,是一片不可读写操作的空间,操作这篇空间会引发段错误。

把零值赋给一个指针,例如:

int *pa;
pa = 0;				//表示指针的状态,什么也不指

查看指针变量指向目标的内容

*指针变量名

修改指针变量指向目标的内容

*指针变量 = 新值

指针变量支持算术运算和关系运算

​ 算术运算支持+ 、 - 、++、–运算,本质上是地址之间的运算

int a = 10, b = 20;
int *p = &a, *q = &b;

p++、q–:表示向地址增大(减小)的方向移动了一个数据类型大小,++、–会引起指针变量保存的值发生改变

#include <stdio.h>
int main(int argc, char *argv[])
{
    int a = 10, b = 20;
    int *p = &a, *q = &b;
    printf("a:%p,b:%p\n",&a, &b);
    printf("p:%p\n",p);
    printf("p->a:%d\n",*p);
    p++;
    printf("p:%p\n",p);
    printf("p->a:%d\n",*p);
    printf("=================\n");
    printf("q:%p\n",q);
    printf("q->b:%d\n",*q);
    q--;
    printf("q:%p\n",q);
    printf("q->b:%d\n",*q);
}

p+n q-n:表示向地址增大(减小)的方向偏移了一个数据类型大小**,**不会引起指针变量保存的值改变

#include <stdio.h>
int main(int argc, char *argv[])
{
    int a = 10, b = 20;
    int *p = &a, *q = &b;
    printf("a:%p,b:%p\n",&a, &b);
    printf("p:%p\n",p);
    printf("p->a:%d\n",*p);
   	printf("p:%p\n",p + 1);
    printf("p:%p\n",p);
    printf("p->a:%d\n",*p);
    printf("=================\n");
    printf("q:%p\n",q);
    printf("q->b:%d\n",*q);
    printf("q:%p\n",q - 1);
    printf("q:%p\n",q);
    printf("q->b:%d\n",*q);
    
    printf("%ld\n", p - q);
    printf("%ld\n", q - p);
}

p-q :表示两指针之间相隔元素个数,结果是整型数据,不是地址量。

  • 没有两指针相加操作

  • 指针进行算术运算,类型必须一致

  • 指针运算多是为数组准备的,单独使用没有太多意义。

关系运算

==、!=,用作两指针是否相等或是否等于NULL

if(q == p)
{
       xxx
}

if(q != NULL)
{
}
const型指针

const用来修饰变量

​ 只能在定义初始化时使用const来修饰。

  1. 常量化变量的值

语法:

const 数据类型 变量名 = 表达式;

使得const所修饰的变量的值不允许修改。

int a = 10;
const int b = 20;

printf(" a = %d\n ", a);
a = 30;
printf(" a = %d\n", a);

printf("b = %d\n", b);
b = 50;								//出现错误,不能修改const修饰的变量值
printf("b = %d\n", b);
  1. 常量化指针变量指向的内容

语法:

const 数据类型 *指针变量名 = 表达式;

使得const所修饰的指针变量指向内容不允许修改。限制通过指针变量修改指向目标的值,但是直接修改指针变量指向目标的内容时允许的,指针变量本身保存的指针也是允许修改的。

int a = 10, b = 20;
const int *p = &a;

printf("a = %d\n", *p);
*p = 20;				//错误的,不允许修改指向目标的内容
a = 30;
printf("a = %d\n", *p);

p = &b;
printf("b = %d\n", *p);
*p = 50;				//更改指针指向后同样不允许修改指向目标的内容
printf("b = %d\n", *p);
  1. 常量化指针变量

语法:

数据类型 * const 指针变量名 = 表达式;

使得const所修饰的指针变量自身保存的内容不允许被修改,只能够一直指向一个目标。

但是指针变量指向目标的内容是可以被修改的。

int a = 10, b = 20;
int *const p = &a;

printf("a = %d\n", *p);
//p = &b;    				错误,指针变量本身不允许被修改
//printf("b = %d\n", *p);

*p = 30;
printf("a = %d\n", *p);

return 0;
  1. 常量化指针变量和指向的内容

语法:

const 数据类型 * const 指针变量名 = 表达式;

使得const所修饰的指针变量自身保存的内容不允许被修改,同时指针变量指向目标的内容也不允许被修改。

int a = 10, b = 20;
const int * const p = &a;
printf("a = %d\n", *p);
//p = &b;				错误,指针变量本身不允许被修改
printf("a = %d\n", *p);
//*p = 30;				错误,指针指向的目标也不允许被修改
printf("a = %d\n", *p);

const修饰函数形式参数时表示传入参数不允许被修改,具体是上边的四种情况之一。

#include <stdio.h>
int main(int argc, char *argv[])
{
	int a[2][3] = {{1,2,3},{2,3,4}};
	int *p = *a;
	int i, j;
	for (i = 0; i < 2; ++i)
	{
		for (j = 0; j < 3; j++)
		{
			printf("p->a[%d][%d] = %d", i, j, *(p + 3 * i + j));
		}
		printf("\n");
	}
	return 0;
}

指针变量自身的数据类型:是除了指针变量名剩下的所有部分。

int *p         		//int *
int p          		//int **
int (*p)[下标]  	   //int (*)[下标]

指针变量指向目标的数据类型:是除了指针变量名加 * ,其余剩下的所有部分。

 int *p         		//int
int **p         		//int *
int (*p)[下标]   		//int [下标]
空类型指针: void *

void *指针变量是一种不确定数据类型的指针变量,它可以通过强制类型转换让void *类型的指针转换为其他任何类型的指针。

定义:

void *指针变量名 = 任意类型指针;

使用:

*(目标指针类型)指针变量名

int a = 10;
void *p = &a;
printf("p->a = %d\n", *(int *)p);
  • 使用空类型指针必须进行强制类型转换,转换为具体的指针类型,同时空类型指针不能直接进行算术运算

用处:

  1. 对数据的类型进行限定

  2. 对函数的参数进行限定

  3. 对函数的返回值进行限定

一级指针与一维数组

​ 一维数组的数组名代表了首元素的首地址**,**这个地址就是一级指针

int a[5];
int *p = a; //a[i]、*(p+i)、*(a+i) 、*p[i]都能访问数组的第i个元素内容。

数组名a 是首元素的首地址,是一个地址常量。

指针变量p是地址变量,保存的内容是地址量。

*(p+i) <==> p[i]

*() <==> [] 变址运算符

字符指针和字符数组

​ 一级指针变量保存地址的指向目标内容是char类型数据。

char str[] = “hello world”;
char *p = str;
printf(%s\n”, p );

字符指针与字符串

char *q = “hello”;

练习:从终端输入7个数据,通过指针保存到数组中,通过指针求所有元素的和,最大值,最小值。

int *p = a;
int i;
for (i = 0; i < 7; ++i)
{
    scanf("%d", p + i);
}
int sum = 0;
for (i = 0; i < 7; ++i)
{
    sum += *(p + i);
    printf("a[%d] = %d, p->%d = %d,a->%d = %d, p[%d] = %d\n", \
          i, a[i], i, *(p + i), i, *(a + i), i, p[i]);
}
int max = *p, min = *p;
for (i = 0; i < 7; ++i) {
    if (*(p + i) > max) {
        max = *(p + i);
    }
     if (*(p + i) < min) {
        min = *(p + i);
    }
    printf("sum = %d, max = %d, min = %d\n", sum, max, min);
    
    return 0;
}

通过字符指针直接指向一个字符串,此时字符串存储在常量区**,**常量区是只读的,不允许修改内容。所有不能够通过字符指针修改指向目标空间的内容。

但是可以修改指针自身的指向,即允许指向另外的地址。

练习:

​ 使用指针实现mystrlen函数。

int mystrlen(char *s)
{
    if (s == NULL) {
        fprintf(stderr, "%s\n", "s is NULL");
        fprintf(stderr, "s is NULL");
    }
    
    int len = 9;
    while(1) {
        if (*s != '\0'){
            ++len;
        } else {
            break;
        }
        s++;
    }
    /*
    int i;
    for (i = 0; *s != '\0'; ++i)
    {
    	len==;
    	s++;
    }
    */
    /*
    for(;*s++ != '\0'; ++len);
    */
    
    return len;
}

int main(int argc, char *argv[])
{
    char str[N];
    char *p = str;
    scanf("%s\n", p);
    printf("%s\n", p);
    
    ine len = mystrlen(p);
    printf("len = %d\n", len);
    len = mystrlen("1234");
    printf("len = %d\n", len);
    
    len = mystrlen(NULL);
    printf("len = %d\n", len);
}
  • 注意:
  1. 当使用指针变量进行++、–的地址偏移操作,指针变量保存的地址会发生改变,不会指向最开始的目标。

如果后续需要最开始的地址,那么需要在操作之前进行地址的备份工作。

  1. 当指针变量作为实际参数传递,形式参数接收的是指针,形式参数保存了这个地址,形式参数通过地址操作对应空间的内容会引起实际参数指向目标内容的改变。

函数的形式参数保存地址发生变化不会对实际参数的地址参数影响。

作业:

  1. 使用指针实现mystrcat、mystrcmp函数。

  2. 定义一个长度为10的整型数组,通过指针从终端获得数据,并且排序。

inclue <strio.h>
char *mystrcpy(char *dest, char *src);
char *mystrcpy(char *dest, char *src)
{
    if (dest == NULL || src == NULL) {
        fprintf(stderr, "指针参数错误,为NULL");
        return NULL;
    }
    printf("dest:%p\n", dest);
    printf("src:%p\n", src);
    char *temp = dest;
    /*
    //src是要拷贝字符的地址,*src是字符内容,如果不是'\0'就进行拷贝工作
    while(*src != '\0') {
    	//把对应src地址的字符内容复制给同位置的dest地址空间保存
    	*dest = *src;
    	//当前字符拷贝完成,需要移动到下一个字符位置准备拷贝
    	//通过++移动地址位置到下一个字符地址
    	src++;
    	dest++;
    }
    //*src表示在src字符为'\0'时退出, 这个'\0'也需要拷贝到对应位置
    *dest = *src;				//*dest = '\0'
    */
    
    /*
    while ((*dest=++ = *src++) != '\0');
    for (; (*dest = *src) != '\0'; ++dest, ++src);
    */
    
    for (; (*dest++ = *src++) != '\0'; );
    printf("dest:%p\n",dest);
    printf("src:%0\n",src);
    
    return temp;
}

int main(int argc, char *argv[])
{
   char str1[20] = "chengxing";
    char str2[20];
    char *s1 = str1, *s2 = str2;
    scanf("%s",s2);
    printf("s1:%s\n",s1);
    printf("s2:%s\n",s2);
    printf("s1:%p\n",s1);
    printf("s2:%p\n",s2);
    
    mystrcpy(s1,s2);
    printf("s1:%s\n",s1);
    printf("s2:%s\n",s2);
    printf("s1:%p\n",s1);
    printf("s2:%p\n",s2);
    return 0;
}

指针与二维数组

行指针(数组指针)

​ 本质是指针,是指向固定大小数组的指针。通过数组指针可以找到一个数组。

数组指针变量

​ 存放数组指针(行地址)的指针变量,也叫行指针变量。

定义:

存储类型 数据类型 (*数组指针变量名)[下标];

存储类型:数组指针本身的存储类型

数据类型:指针指向数组空间中数组元素的数据类型

下标:指向数组的数据元素的个数

数组指针变量类型: 数据类型 (*)[下标]

#include <stdio.h?
int main(int argc, char *argc[])
{
	int a[3] = {1,2,3};
	int (*p)[3] = &a;
    printf("a:%p\n",a);
    printf("&a:%p\n",&a);
    
    printf("p:%p\n",p);
    printf("p + 1:%p\n",p + 1);
    printf("&p:%p\n",&p);
    printf("*p:%p\n",*p);
    printf("**p:%p\n",**p);
}
  • 方括号中的常量表达式表示指针加1,移动几个数据。
  • 当用行指针操作二维数组时,表达式一般写成1行的元素个数,即列

指针与字符串

字符指针与字符串

C语言通过使用char数据类型的数组来处理字符串。

在字符数组中,每个数组元素都是char数据类型的变量。通常,我们把char数据类型的指针变量称为字符指针变量。字符指针变量与字符数组有着密切关系,它也被用来处理字符串。

在程序中,初始化字符指针是把内存中字符串的首地址赋予指针,并不是把该字符串复制到指针中。另外,向字符指针赋给一个字符串常量时,指针应该指向一个存储空间。

在C编程中,当一个字符指针初始化为指向一个字符串常量时,不能对字符串指针变量的目标赋值。

指针数组

​ 本质是数组,是指若干个具有相同存储类型和数据类型的指针变量构成的集合,数组中的元素是指针。

定义

存储类型 数据类型 *指针数组变量名[下标];

存储类型:指针数组中指针元素的存储类型

数据类型:指针数组中指针元素指向目标的数据类型

下标:有多少个指针元素

指针元素本身数据类型:数据类型 *

int a = 10, b = 20, c = 30;
int *arr[3] = { &a, &b, &c};

printf("&a:%p\n", &a);
printf("&b:%p\n", &b);
printf("&c:%p\n", &c);

printf("arr[0]:%p\n", &arr[0]);
printf("arr[1]:%p\n", &arr[1]);
printf("arr[2]:%p\n", &arr[2]);

printf("*(arr[0]):%d\n", &arr[0]);
printf("*(arr[1]):%d\n", &arr[1]);
printf("*(arr[2]):%d\n", &arr[2]);
  • 指针数组变量名就表示该指针数组的存储首地址,即指针数组名为数组
int a[3] = {1, 2, 3}, b[3] = {4, 5, 6}, c[3] = {7, 8, 9};
int *arr[3] = {a, b, c};

//arr[0]、arr[1]\arr[2]分别是三个数组的首地址
//*(arr[0])等价于a[0], *(arr[1])等价于*(arr[0] + 1)等价于a[1]
int i,j;

for (i = 0; i < 3; ++i)				//找三个数组的首地址
{
    for (j = 0; j < 3; ++j)			//找当前数组每个元素的地址
    {
        //(arr[i] + j)表示具体的第几个数组的第几个元素地址
        //*(arr[i] + j)表示具体的第几个数组的第几个元素内容
        printf("arr[%d]->%d = %d\n", i , j, *(arr[i] + j));
    }
}

for (i = 0; i < 3; ++i)				//找三个数组的首地址
{
    for (j = 0; j < 3; ++j)			//找当前数组每个元素的地址
    {
        //(arr + i)表示具体的arr数组首元素首地址偏移i个元素,结果是指针
        //*(arr + i)表示拿arr + i指针指向目标的内容,这个内容就是arr数组的第i个元素,这个元素也是一个指针,这个指针指向a,b,c数组首元素的首地址
        //*(arr + i) + j表示第i个数字的第j个元素的地址
        //*(*(arr + i) + j)表示具体的第i个数字的第j个元素内容
        printf("arr[%d]->%d = %d\n", i , j, *(*(arr + i) + j));
    }
}

一级指针操作二维数组

#include <stdio.h>
int main(int argc, char *argv[])
{
	int a[2][3] = {{1, 2, 3},{4, 5, 6}};
	int *p = *a;
	int i,j;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("p->a[%d][%d]\n = %d\n",i, j, *(p + i + j));
		}
		printf("\n");
	}
	return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
	int a[2][4] = {{1, 2, 3, 4},{5, 6, 7, 8}};
	int *p = &a[0][0];				//&a[0][0]这个地址是一个一级指针
    int *p = a[0];					//第一个一维数组元素中首元素的首地址与&a[0][0]一样
	int i,j;
	for (i = 0; i < 2*4; i++)
	{
		printf("p->%d = %d\n",i, *(p + i));
		printf("\n");
	}
	return 0;
}
二级指针

​ 指针的指针,存放地址的地址**。一个指针(1)指向另一片内存空间,**这片内存空间中存放的还是一个指针(2),这个指针(2)指向的内存空间存放真实的数据。

二级指针变量

​ 存放指针的指针的变量。

定义:

存储类型 数据类型 **二级指针变量名;

存储类型:指针变量自身的存储类型

数据类型:二级指针变量指向目标的数据类型 : 数据类型 *

二级指针变量数据类型:数据类型

int a[3] = {1,2,3},b[3] = {4,5,6},c[3] = {6,7,8};
int *arr[3] = [a,c,c];
int **p = arr;

int i,j;
for (i = 0; i < 3; ++i)
{
    for (j = 0; j < 3; ++j)
    {
        //p					表示首元素的首地址,&arr[0]
        //p + i				表示第i个元素的首地址
        //*(p + i)			表示第i个元素的内容,但是这个内容是一个一级指针,是一个地址,指向一个数组首元素,是首元素的首地址
        //*(p + i) + j		表示第j个元素的首地址
        //*(*(p + i) + j)	表示第i个元素存放内容指向数组第j个目标的内容
        printf("p->%d->%d = %d\n", i, j, *(*(p + i) + j));
    }
}
字符二维数组和字符串

​ 数据类型指定为char的二维数组,存放字符数据

使用字符串初始化方式

char str[2][50] = {{“hello”}, {“world”}};

字符串结束标志:'\0'

某班有至多30人参加期末考试。科目为语文,数学,英语。

用二维数组实现一下功能:

1.输入学生的各科考试成绩(姓名,三科成绩)name[30][20]; score[30][3];

2.计算每个学生的平均分 average[30];

3.输出每名学生的各科成绩和个人平均分

4.计算每科的平均分,最高分,最低分,及格率(0~10,6, 0~100,60)

输出格式有如下要求:

张三 王五 李四 赵雷

语文 75 88 100 92

数学 xx xx xx xx

英语 xx xx xx xx

函数

函数是一个完成特定功能的代码模块,其程序代码独立 ,通常要求有返回值,也可以是空值。

#include <stdio.h>
int add(int a, int b);
int add(int a, int b) {
	printf("add:a = %d, b = %d\n", a, b);
    a++;
    b++;
    printf("add:a = %d, b = %d\n", a, b);
    printf("a:%p, b:%p\n", &a, &b);
    return (a + b);
}

int main(int argc, char *argv[]) {
    int x = 10, y = 20;
    printf("add:x = %d, y = %d\n", x, y);
    int sum = add(x , y);
    printf("sum = %d\n", sum);
    printf("add:x = %d, y = %d\n", x, y);
    printf("x:%p, y:%p\n", &x, &y);
    return 0;
}

语法结构

返回值数据类型 函数名称(形式参数列表)
{
	函数体,功能语句部分;
	可选的return [(表达式)];
}    

其中:

大括弧对{<语句序列>},称为函数体;

函数名称:是一个标识符,要求符合标识符的命名规则;

形式参数列表:函数在执行时,需要什么数据类型的数据,指一类的数据;以逗号“,”分隔的多个变量的说明形式,通常称为形参;

返回值数据类型:返回值数据类型指返回数据的数据类型,函数执行完可能有返回的结果给调用者,如果无返回值应该写为void型;

  • return [(表达式)]语句中表达式的值,要和函数的返回值数据类型保持一致;如果函数的返回值数据类型为void可以省略或者无表达式结果返回(即写成return;)

函数的声明

函数的声明就是指函数原型,告诉编译器存在这个函数,告诉调用者函数使用方式,需要给什么类型数据,函数执行完会返回什么类型数据;

语法结构

返回值数据类型 函数名称(形式参数列表);

  • 其中:形式参数列表可以缺省说明的变量名称,但类型不能缺省

    例如,

    double add(double x, int y);
    double add(double, int);
    

函数的调用

语法结构:

函数名称(实际参数列表);

其中:

函数名称:是一个标识符,要求符合标识符的命名规则;

实际参数列表:实际参数列表事需要提供具体的数据;如果该函数提供返回值,同时需要返回数据,定义相同数据类型变量来接收。实参就是在使用函数时,调用函数传递给被调函数的数据,用以完成所要求的任务。

注意:

  • 函数调用可以作为一个运算量出现在表达式中,也可以单独形成一个语句。对于无返回值的函数来讲,只能形成一个函数调用语句。

  • 函数定义时的形式参数列表和返回值数据类型**都可以省略为空,**当为空时使用void来表示

  • 当形式参数列表为空时,调用该函数实际参数列表不填

在单文件中,函数声明省略时,函数定义必须在函数调用上方。

函数的参数传递方式

  1. 值传递

函数调用传递实际参数,传递的是参数的值,函数的形式参数把值复制一份,操作形式参数【不会改变实际参数】的值。(将实参的数据拷贝给了形参变量,实参和形参是具有相同数据类型但存储空间是不同的两组空间)。

​ 复制传递方式是函数间传递数据常用的方式。调用函数将实参传递给被调函数,被调函数将创建同类型的形参并用实参初始化。即把实参赋给一个新的变量,把实参复制到新建形参的存储区域中。被调用函数处理的数据是复制到其形参的数据,因此,即使改变形参的值也不会影响实参的值,一旦被调用函数完成了其任务时,这些形参通常就释放其占用空间。

  1. 地址传递

​ 函数调用传递实际参数,传递的是【参数的地址】,形式参数拿到地址,此时形式参数和实际参数【指向同一片内存空间】,操作形式参数改变实际参数的数据。

​ 调用函数将实参的地址传递给被调用函数,被调用函数对该地址的目标操作,相当于对实参本身操作。按地址传递实参为变量的地址,而形参为同类型的指针。

  1. 全局变量

    全局变量就是在函数体外说明的变量,它们在程序中的每个函数里都是可见的。实际上,全局变量也是一种静态型的变量。将它初始化为0。全局变量一经定义后就会在程序的任何地方可见。使用全局变量传递数据的先后顺序的不同会影响计算结果,应用顺序不当,会导致错误,这种方式尽量少用。

    int n;						//定义一个全局变量n
    double factorial(void);		//说明了一个double型的函数factorial
    int main()
    {
        double s = 0;
        n = 10;
        s = factorial();
        printf("%e\n",s);
    }
    
    double factorial(void) {
        double r = 1;
        int i;
        for (i = 1; i <= n; i++) {
            r *= i;
        }
        return (r);
    }
    

函数的传参–数组

  1. 复制传递方式

    函数与函数之间的数组传递,复制传递方式只是提供一种形式,被调用函数的形参数组的数组名实际上是一个指针变量,因此,复制传递方式与数组的指针传递方式完全相同,只是形参的说明形式不同而已。调用函数将实参数组传递给被调用函数形参,形参接收是实参的地址,被调用函数里对形参数组的操作会影响调用函数里的实参数组。

    double TestArray(double b[], int size);
    int main()
    {
        int i;
        double a[5] = {1.1,2.1,3.1,4.1,5.1}, r = 0;
        r = TestArray(a,5);
        printf("%f\n",r);
        r = 0;
        for (i = 0; i < 5; i++)
        {
            r += a[i];
        }
        printf("%f\n",r);
        printf("%f\n",a[4]);
    }
    double TestArray(double b[], ing size)
    {
        double s = 0;
        int i;
        for (i = 0; i < size; i++)
        {
            s += b[i];
            b[4] = 0;
        }
        printf("%f\n",b[4]);
        return (s);
    }
    
  2. 地址传递方式

    函数与函数之间数组的地址传递方式是将调用函数中的实参数组的起始地址传递给被调用函数的指针变量形参。因此,被调用函数中对形参地址的目标操作,相当于对实参本身的操作,将直接改变数组的值。地址传递方式,实参应该为数组的指针,而形参应为同类型的指针变量。另外,数组的地址传递方式不仅要把实参数组的指针作为参数传递给函数,同时还需要传递数组的长度。

    double TestArray(double *pa, int size);
    int main()
    {
    	double a[5] = {1.1,2.1,3.1,4.1,5.1}, r = 0;
        r = TestArray(a,5);
        printf("%.2f\n",r);
    }
    
    double TestArray(double *pa, int size)
    {
        int i;
        double s = 0;
        for (i = 0; i < size; ++i)
            s += pa[i];
        return (s);
    }
    
多维数组与函数
指针函数

​ 指一个函数的返回值数据类型是指针类型,这样的函数就是指针函数,即一个函数的返回值是某一数据类型变量的地址。

指针函数定义形式:

 返回值数据类型 *函数名(形式参数列表)
{
	函数体
	return	指针;
}

返回值:static变量的地址/全局变量的地址/字符串常量的地址/堆的地址

  • 普通局部变量的地址不允许返回

其中

​ 在函数名之前的*符号,说明该函数返回一个地址量。

char *InverseString(char *pstr);
int main()
{
	char str[6] = {'a','b','c','d','e','\0'};
    printf("%s\n",InverseString(str));
}

char *InverseString(char 8pstr)
{
    static char text[128];
    int len = 0;
    int i;
    while (*(pstr + len) != '\0')
        len++;
    
    for (i = len - 1; i >= 0; --i)
        text[len - i - 1] = *(pstr + i);
    text[len] = '\0';
    return (text);
}
函数指针

​ 本质是指针,是一个指向目标为函数的指针,存放的是函数的入口地址,而且是函数调用时使用的起始地址。当一个函数指针指向了一个函数,就可以通过这个指针来调用该函数,函数指针可以将函数作为参数传递给其它函数调用。

函数名的意义:

  1. 代表了函数本身

  2. 代表了函数的入口地址

函数指针变量定义:

返回值数据类型 (*函数指针变量名)(形式参数列表);

返回值数据类型:函数指针指向函数的返回值数据类型

形式参数列表:函数指针指向函数的形式参数列表

函数指针变量本身的类型:返回值数据类型 (*)(形式参数列表)

  • (*函数指针变量名)中,*说明为指针,()不可缺省,表明为函数的指针。

函数指针使用:

  1. 函数指针指向一个函数

    int add(int a, int b)
    {
    	return a + b;
    }
    
    int main(int argc, char *argv[])
    {
        int (*p)(int, int) = add;
        printf("sum = %d\n", sum);
        //通过函数指针调用函数,(*函数指针变量名)(实际参数)
        sum = (*p)(1,2);
        printf("sum = %d\n", sum);
        //(函数指针变量名)(实际参数)
        sum = (p)(3,4);
        printf("sum = %d\n", sum);
        return 0;
    }
    
  2. 函数指针作为函数的参数传入

    void show(void)
    {
    	printf("+++++++++++++++++++++\n");
    }
    int add(int a, int b,void (*fun_p)(void))
    {
    	fun_p();
    	return a + b;
    }
    
    int main(int argc, char *argv[])
    {
        int (*fun_p)(int, int, void(*)(void)) = add;
        void (*fun_q)(void) = show;
        printf("sum = %d\n", sum);
        //通过函数指针调用函数,(*函数指针变量名)(实际参数)
        sum = (*fun_p)(1,2,fun_q);
        printf("sum = %d\n", sum);
        //(函数指针变量名)(实际参数)
        sum = (p)(3,4,fun_q);
        printf("sum = %d\n", sum);
        return 0;
    }
    
char *mystrcpy(char *dect, const char *src, char *(*fun_p)(char *, const char *))
{
    if (dest == NULL || src == NULL)
        return NULL;
    char *temp = fun_p(dest, src);
    return remp;
}

int main(int argc, char *argv[])
{
    char str[50];
    char *(*fun_p)(char *, const char *, char *(*fun_p)(char *, const char *)) = mystrcpy;
    char *(*fun_q)(char *, const char *) = strcpy;
    
    ptintf("%p\n", str);
    char *s = fun_p(str, "chengxing", fun_q);
    
    printf("%p\n", s);
    printf("%s\n", str);
    
    return 0;
}
#include <stdio.h>
int test(int a, int b);				//函数声明
int Minus(int, int );				//函数声明,缺省形式参数名称

int main()
{
    int x = 5, y = 8;
    int (*pFunc)(int a, int b);				//声明一个名称为pFunc的函数指针
    pFunc = Plus;							//把函数Plus的地址赋给函数指针pFunc;
    printf("%d\n",(*pFunc)(x, y));
    pFunc = Minus;							//把函数Minus的地址赋给函数指针pFunc;
    printf("%d\n",(*pFunc)(x, y));
    printf("%d\n",test(15,5,Plus));			//把函数Plus作为实参调用test
    printf("%d\n",test(15,5,Minus));
}

int Plus(int a, int b)
{
    return (a + b);
}
int Minus(int a, int b)
{
    return (a - b);
}
int test(int a,int b, int (*pFunt)(int m, int n))
{
    return ((*pFunt)(a, b));
}
  1. 函数指针作为函数的返回值

    //返回函数指针指向函数的返回值类型 (*函数名称(函数形式参数列表))(返回函数指针指向函数的形式参数列表)
    int (*cal(char symbol))(int, int)
    {
    	switch (symbol) {
            case '+':
                return add;
                break;
             case '-':
                return sub;
                break;
             case '*':
                return mul;
                break;
             case '/':
                return div;
                break;
        }
        return NULL;
    }
    int main(int argc, char *argv[])
    {
        int x, y;
        char symbol;
        printf("请输入数据:\n");
        scanf("%d%1c%d",&x, &symbol, &y);
        
        //cal(symbol)返回的就是函数指针
        //int (*func_p)(int, int) = cal(symbol);
        //func_p(x, y);
        //上边两步操作与下边完全等价
        int value = cal(symbol)(x ,y);
        
        printf("value = %d\n", value);
        
        return 0;
    }
    

函数指针数组

函数指针数组是一个保存若干函数名的数组

语法:

存储类型 数据类型 (*函数指针数组名称[大小])(参数列表);

其中:<大小>是指函数指针数组元素的个数,其它同函数指针。

#include <stdio.h>
int Plus(int a, int b);
int Minus(int a, int b);
int main()
{
    int (*pFunc[2])(int a, int b);
    int i;
    
    pFunc[0] = Plus;
    pFunc[1] = Minus;
    
    for (i = 0; i < 2; i++)
        printf("%d\n",(*pFunc[i])(15, 85));
}

int Plus(int a, int b) {
    return (a + b);
}
int Plus(int a, int b) {
    return (a - b);
}

回调函数

​ 自己定义的函数作为参数传递给其他函数使用,由其他函数在运行时通过参数传递的方式调用,被调用的方式就是回调函数。

​ 在传递回调函数时传递的是函数指针。

递归函数

​ 在一个函数体内部直接或者间接调用了函数本身,这样的函数被称为递归函数。这里的直接调用是指一个函数的函数体中含有调用自身的语句,间接调用是指一个函数在函数体类有调用了其它函数,而其它函数又反过来调用了该函数的情况。

递归函数调用执行的两个阶段:

  1. 递推阶段:从问题出发,按照递推公式从未知推导到已知,最终达到递归的终止条件

  2. 回归阶段:按照终止条件,逆向逐步带入递推公式,回归问题求解

int factorial(int n)
{
	if (n == 0 || n == 1)
	{
		return 1;
	}
}

int sum(int n)
{
	if (n == 1)
	{
		return n;
	}
	return n + sum( n - 1);
}

写一个递归函数必须有一个明确的退出条件和一个调用函数自身完成的递推阶段。

递归函数的缺点

  1. 执行效率低,使用内存多

  2. 使用递归函数必须由明确的结束条件,否则会造成死循环,栈会溢出。

bool数据类型

​ 逻辑数据类型,在内存中占据1字节大小

true (非零)

false(零)

在C99中关键字是 _Bool

使用方式:

  1. 字节使用_Bool定义变量,变量赋值采用数值型数据操作,非0为true,0为fasle

  2. 添加 stdbool.h ,直接使用true和fasle

标识常量:宏

只做符号的替换工作

#define 宏名 常量/表达式

宏函数:宏函数不是真正的函数,还是符号替换工作

#define add(x, y) ((x)+(y))

使用宏函数时,所有参数都要加小括号,整体也要加小括号

多文件编程

创建多文件流程

vim xx.c xx.c xx.h -O/o/p

保存:xall/xa/wqa/wqall

main.c 包含main函数的文件,主要是系统流程架构

xx.c 功能文件,包含自己写的函数/全局变量

xx.h 通常与.c文件同名,用作函数的声明/宏/全局变量的声明,这个文件可以看作是有什么东西可以被别人使用

条件编译语法格式:

#ifndef 宏名 //宏名通常是文件名 __XXX_H__

#define 宏名 //名字与上边保持一致

包含的内容:

#endif //表示包含结束

main.c

#include <stdio.h>				//<>从系统制定路径查找文件
#include "add.h"				//""从当前路径查找文件

int main(int argc, char *argv[])
{
    int a, b;
    printf("请输入两个数:");
    scanf("%d&d", &a, &b);
#if 1
    int sum = add(a, b);
#endif
	printf("sum = %d\n", sum);
    
    return 0;
}

add.c

int add(int x, int y) {
	 retuen (x + y);
}

add.h

#inndef __ADD__H__
#define __ADD__H__

int add(int x, int y);

#dedif				//ADD__H__
//条件编译内容

存储类型

存储类时期作用域链接声明方式
自动自动代码块代码块内
寄存器自动代码块代码块内,使用关键字register
具有外部链接的静态静态文件外部所有函数之外
具有内部链接的静态静态文件内部所有函数之外,使用关键字static
空链接的静态静态代码块代码块内,使用关键字static

自动存储类auto:

​ 在一个代码块中声明的变量(或者是在函数头部作为参数),无论有无auto修饰,都属于自动存储类

​ 自动存储类具有自动的存储周期**、**代码作用域和空链接

​ 如果未初始化,其值是随机值

​ 代码块内和函数头部定义的变量均属于auto类

​ 生命周期与作用域:

​ 均是从定义的开始到模块的结束

​ 空链接**:**无法被其他模块或文件使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-myJ3Q5CQ-1648799121434)(ipic%5Cclip_image004-1647874421132.jpg)]

内存模型:自由存储区、静态区、动态区

根据C\C++生命周期不同,C\C++的内存模型有三种不同,即自由存储区、动态区、静态区、动态区

自由存储区:局部非静态变量的存储区域,即平常所说的栈

动态区:用new、malloc分配得内存,即平常所说的堆

静态区:全局变量、静态变量、字符串常量存在的位置

注:代码虽然占内存,但不属于C\C++内存模型的一部分

寄存器类register:

​ 该类存储在CPU内部,无法取得地址。

​ register类在存储时,如果没有多余的寄存器分配,那么就是按照auto类型存储。

​ 寄存器类与自动存储类相似,具有自动存储周期、代码作用域和空链接。

​ 生命周期与作用域:

​ 均是从定义的开始到模块的结束。

​ 空链接:无法被其他模块或文件使用

静态外部链接类:

​ 在所有函数外部定义的,未使用static修饰的变量,具有静态外部链接存储类。

​ 普通的全局变量属于此类。

​ 其他文件使用时需要使用extern关键字声明。

​ 生命周期与作用域:

​ 均是从程序的开始到程序的结束。

​ 外部链接**:**可以被其他模块或文件使用(加extern关键字声明),属于整个程序

静态内部链接类:

​ 使用static声明的,定义在函数外部的变量,具有静态内部链接存储类,由static修饰的全局变量属于此类。

生命周期与作用域:

​ 生命周期是从程序开始到程序结束**,**作用域是本文件

内部链接**:**只能被本文件内的其他模块使用

静态空链接类:

​ 在一个代码块/模块内使用static修饰的变量,属于静态空链接类,由static修饰的局部变量属于此类。

生命周期与作用域:

​ 生命周期是从定义开始到程序结束,作用域是模块内

​ 空链接:不能被其他模块或文件使用

​ 由static修饰的局部变量只会被初始化一次(普通局部变量每次执行都会被初始化)

auto类变量(普通局部变量),未初始化,其值是随机值,存储在栈区**,**系统自动开辟自动释放

静态外部链接存储类(普通全局变量),未初始化,其值为0,存储在.bss

静态内部链接存储类(static修饰的全局变量),未初始化,其值为0,存储在.bss

静态空链接存储类(static修饰的局部变量),未初始化,其值为0,存储在.bss

存储类型的四种声明

auto:自动存储类,默认存储类型。

register:寄存器存储,定义的变量放在cpu内部。如果申请不到,就视为auto。

static:静态存储类型

​ static修饰的变量只会被初始化一次。

​ 修饰局部变量,作用域是本模块。

​ 修饰全局变量,作用域变为本文件。

​ 修饰函数(在函数的定义和声明处使用,返回值类型前加static修饰),限制函数的使用范围,只能够在本文件内使用。

extern:外部参照引用类型

​ 用作已经存在变量的声明。不是定义变量,是声明引用变量。

  1. 只能够修饰普通全局变量

  2. 修饰的全局变量必须已经定义

  3. 多文件同时编译

gcc编译器

​ gcc编译器支持的文件后缀

​ .c C语言源码文件

​ .C/.cc/.cxx C++语言源码文件

​ .i 经过预处理的C语言文件

​ .s 汇编文件后缀

​ .h 头文件

​ .o 目标文件

​ .so 编译后的库文件

编译C语言的阶段

​ 预处理阶段

​ 将源码文件预处理

​ gcc -E xx.c -o xx.i

​ 编译阶段

​ 生成汇编代码,检查语法错误

​ gcc -S xx.i -o xx.s

​ gcc -S xx.c -o xx.s

​ 汇编阶段

​ 生成目标代码

​ gcc -c xx.s -o xx.o

​ gcc -c xxx.c -o xxx.o

​ 链接阶段

​ 将目标文件链接生成可执行程序

​ gcc xx.o -o xxx

​ 运行./xxx

结构体

结构体属于构造数据类型,是由用户自定义的新的数据类型

解决不能描述复杂信息,多项信息的数据类型不同的场合。

结构体类型定义:

struct 结构体数据类型 {
	数据类型 成员1;
	数据类型 成员2;
	 …
	数据类型 成员n;
};

结构体数据类型的定义通常是放在函数模块外边。

struct student_t {
	char name[20];
	int age;
	float weight;
};

定义结构体变量必须要有对应的结构体数据类型。

定义结构体变量名:

存储类型 struct 结构体数据类型 结构体变量名;

成员的表示

结构体变量名.成员名

示例:

  1. 只定义
struct student_t st;
  1. 定义并初始化
struct student_t st = {“张三”, 20, 56.7};
  1. 先定义后赋值
struct student_t st;
strcpy(st.name, “李四” );
st.age = 22;
st.weight = 66.6;

结构体的操作是对结构体的每个成员项单独操作,而且遵循成员项自身的操作规则

结构体的嵌套使用

​ 如果结构体成员又是结构体类型,使用成员运算符一级一级的找到最低级的基本成员为止:

结构体变量名.结构体成员.成员…

结构体数组

​ 本质是数组, 是相同结构体数据类型元素的集合

定义:

存储类型 struct 结构体数据类型 结构体数组变量名[下标];

struct student_t {
	char name[20];
    int age;
    float weight;
};

int main(int argc, char *argv[])
{
    struct student_t arr_st[3] = {0};
    
    int i;
    for (i = 0; i < 3; ++i)
    {
        printf("%d:name:", i);
        scanf("%s", arr_st[i].name);
        printf("%d:age:", i);
        scanf("%d", &arr_st[i].age);
        printf("%d:weight:", i);
        scanf("%f", &arr_st[i].weight);
    }
    for (i = 0; i < 3; ++i)
    {
        printf("%d:name:%s\n", i, arr_st[i].name);
        printf("%d:age:%d\n", i, arr_st[i].age);
        printf("%d:weight:%.1f\n", i, arr_st[i].weight);
    }
    
    return 0;
}

结构体指针

​ 本质是指针,指向目标是结构体类型空间

定义:

存储类型 struct 结构体数据类型 *结构体指针变量名;

找到结构体指针指向结构体成员数据

(*结构体指针变量名).成员名 or

`结构体指针变量名->成员名`

->和 (* ) . 完全等价,通常是使用 ->

struct student_t {
	char name[20];
    int age;
    float weight;
};

int main(int argc, char *argv[])
{
    struct student_t arr_st[3] = {{"LIMANMAN",23,45.6},{"JUNMO",34,56.1},{"LINWU",25,66.6}};
    struct student_t *st_p = arr_st;
    
    for (int i = 0; i < 3; ++i)
    {
        printf("%d:name:%s\n", i, arr_st[i].name);
		printf("%d:name:%s\n", i, (*(arr_st + i)).name);
		printf("%d:name:%s\n", i, (arr_st + i)->name);
 		printf("%d:name:%d\n", i, arr_st[i].age);
		printf("%d:name:%d\n", i, (*(arr_st + i)).age);
		printf("%d:name:%d\n", i, (arr_st + i)->age);
 		printf("%d:name:%.lf\n", i, arr_st[i].weight);
		printf("%d:name:%.lf\n", i, (*(arr_st + i)).weight);
		printf("%d:name:%.lf\n", i, (arr_st + i)->weight);
    }
    return 0;
}

结构体指针数组

​ 本质是数组,数组中存储的数据元素是结构体指针类型。

定义

存储类型 struct 结构体数据类型 *结构体指针数组变量名[下标];

存储类型:数组中结构体指针元素存储类型

下标:元素的个数

typedef 的使用

​ 给已经存在的数据类型取别名

语法:

typedef 已有数据类型 新的别名;

​ 新的别名和已经存在的数据类型完全一致,别名只是一个声明标识

typedef和define 的区别:

​ define 只是做替换工作,只能给常量替换,在预处理时就会替换

​ typedef 只能给数据类型替换

typedef struct student {
	char name[20];
	int age;
} student_t;

int main(int argc, char *argv[])
{
    student_t st1 = {"chengxing",22};
    printf("name:%s\n", st1.name);
   	printf("age:%d\n", st1.age);
    
    return 0;
}
(*(void(*func)(void))0)();
*(void(*func)(void))0
void(*func)(void)

(void(*func)(void)) 0

函数指针,指向一个形参为void,返回值为void的函数

*(void(*func)(void)) 0

将数值数据0强制转换为void(*func)(void)类型数据:函数指针类型数据

(*(void(*func)(void))0)()

加*取内容, 找到一个函数

(*(void(*func)(void))0)();

通过函数指针调用函数

代表从地址为0的位置执行程序,通过将整型常量0强制转换为 形参为void,返回值类型为void的函数指针,通过该函数指针调用。

结构体大小

​ 在不考虑字节对齐的情况下,结构体数据类型变量所占据的内存空间大小是各成员项占据空间总大小

字节对齐

​ 规则:先按照数据类型本身对齐,然后按照整个结构体进行对齐,对齐值必须是2的整数次幂。 取消字节对齐–__attribute__((packed))

struct 结构体数据类型名{
	成员列表;
}__attribute__((packed));  //两个_

共用体

​ 在定义、声明和使用形式上都和结构体基本类似,区别仅仅是在内存存储上的不同。

​ 共用体与结构体同样用作表示复杂数据类型的数据,但是共用体是多个成员共用同一片内存空间。

定义:

union 共用体数据类型{
	成员列表;
};

所有的数据成员在内存中使用相同的内存空间。空间的大小是根据共用体中成员数据长度最大的成员决定。

共用体存储内容是按照最后一次操作共用体成员内容确定的

枚举

​ 将一段有序的整数值放入一个集合中

定义

enum 枚举类型{
	成员 1 = 整数值,
	成员 2,
	成员 3,
	…
	成员 n
};

如果第一个成员值为n,那么后边的成员值依次加1

通常使用给枚举类型来定义状态标志、错误码、顺序序列

定义枚举类型变量

enum 枚举类型 枚举变量名 = 枚举类型成员;

给枚举类型变量赋值只能给枚举类型中存在的成员。

#include <stdio.h>
enum ERRORStatus {
	ERROR = 0,
	SUCCESS = !error
};
int main(int agrc, char *argv[])
{
    enum ERRORStatus status;
    if (1)
    {
        status = SUCCESS;
        printf("%d\n", status);
    } else {
        status = error;
        printf("%d\n", status);
    }
    return 0;
}

运行结果:1

字节序

​ 主要是平台差异性,涉及大小端问题

​ 大端存储:数据的高字节放在低地址,低字节放在高地址

​ 小端存储:数据的高字节放在高地址,低字节放在低地址

如何验证本机字节序是大端还是小端

通过 char 类型数据占1字节,所有无论是大端还是小端数据都不会有变化;共用体所有成员共用同一片内存空间。

#include <stdio.h>
typedef union test {
	unsigned char a;
    unsigned int b;
} test_t;

int main(int argc, char *argv[])
{
    test_t t;
    t.b = 10;
    printf("a = %d\n", t.a);
    printf("b = %d\n", t.b);
    //如果a为10就是小端存储,如果a为0就是大端存储
    
    return 0;
}

动态内存管理

  1. 静态内存分配

    ​ 定义变量时编译器在编译过程中可以根据变量的类型来决定存储区域和内存空间大小,并且给它们分配合适的内存空间。

​ 局部变量,在栈区自动创建自动释放。

​ 2. 动态内存分配

​ 有些操作对象只有在程序运行时才能确定,编译器无法在编译时提前预定内存空间。只有在程序运行过程中,根据实时情况进行内存分配。

​ 所有的动态内存分配都是在堆区进行的**。**堆区的空间由程序员手动申请手动释放。

​ 3. 申请与释放方式

​ 程序员在程序运行时通过malloc函数申请任意大小的内存,通过free函数释放内存。动态内存的生命周期由程序员灵活决定。

​ 当程序运行到需要一个动态分配的变量或者对象时,此时向系统申请堆空间用于存储变量或对象。当不再需要这个变量或对象时,要显式的释放它所占用的空间。这样系统可以对这部分空间重新分配,达到循环使用的目的。

​ 特别注意的是,被分配的堆空间其内容是随机的,所有使用前需要初始化。

malloc/free函数

 头文件:#include <stdlib.h>
函数原型:
void \*malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

malloc作用:开辟指定大小的空间

参数:要申请空间大小,单位字节

返回值:成功,返回申请到的空间首地址,失败,返回NULL

free:

参数:要释放空间的首地址

calloc: 作用:开辟指定大小的空间,常常用作数组类型空间开辟

参数:指定开辟空间的数量(nmemb)和每个空间所需字节数(size)

返回值:成功,返回申请到的空间首地址,失败,NULL

realloc:作用:调整之前malloc或者calloc开辟空间的大小

参数:要调整的堆空间指针(void *ptr),和新空间的大小(size)

返回值:成功,返回申请到的空间首地址,失败,NULL

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
	//malloc开辟空间只需要字节数,需要定义空间存储数据对应指针变量来接收,同时malloc需要做强制类型转换。
    int *p = (int *)malloc(4);
    //当获得开辟空间首地址时,需要做判断再使用
    if (p == NULL)
    {
        fprintf(stderr, "p is NULL\n");
        //return -1;
        exit(1);
    }
    printf("p = %p\n", p);
    *p = 50;
    printf("p-> = %d\n", *p);
    
    char *s = (char *)p;
    p = NULL;
    
    strcpy(s,"chengxing");
    printf("p-> = %s\n", *p);
    
    free(p);
    printf("p = %d\n", p);
    printf("s = %s\n", s);
    //将释放空间的指针指向为NULL,预防形成野指针
    s = NULL;
}

make工程管理器

make介绍

​ 只要用作管理众多的工程文件,或者文件数量较多的场合。

make组成

  1. make命令

  2. makefile文件

make命令执行时需要一个make文件,make文件的作用是告诉make命令需要怎样去编译和链接程序。

make的好处是自动化编译,只要有makefile文件,只需make命令就能使工程自动编译,提高了开发效率。

makefile文件基本组成

​ makefile文件的名称是makefile或Makefile.

​ 基本组成:

目标:依赖/关联

命令/动作

​ 目标:通常是可执行文件、目标文件或标签。。。

​ 依赖/关联:生成目标所依赖的文件或者目标。

​ 是必不可少的

如果关联/依赖有一个以上的文件比目标文件要新,命令/动作就会被执行,这是makefile的基本规则也是最核心的内容。

make工作流程

​ 从输入make命令开始

  1. make会在当前目录下寻找文件名为makefile 或者是Makefile文件

  2. 找makefile文件中第一个目标文件A

  3. 如果说目标A文件是不存在的,或者目标A依赖的文件B修改时间比目标A文件要新,那么make就会自动执行命令/动作来生成目标A文件

  4. 如果目标A文件所依赖的文件B也不存在,那么make或在当前文件中找到关联文件B的依赖关系,如果找到了文件B的依赖,根据规则生成文件B。

  5. 如果对应的.c和.h文件都存在时,make会生成依赖文件B,然后关联文件B生成目标文件A,完成make任务。

makefile 文件中默认第一个目标为最终生成的目标文件

执行指定目标

​ make 目标名

特别注意当文件名和目标名一致时会发生冲突。

创建伪目标

.PHONY

作用:防止存在和目标名一致的文件名,导致 make 目标 不能执行

使用方式:

​ .PHONY:目标名

​ 目标名:

​ 命令/动作

​ 示例:

​ .PHONY:clean

clean:

​ rm xx xx xx xx

#作为注释标志,注释一行

make变量

​ make变量是类似于C语言的宏,是替换字符串

变量的定义

​ 即时变量: 变量名:=内容 #变量的值在定义时立即确定

​ 延时变量: 变量名=内容 #变量的值在使用时才确定

使用变量

​ $(变量名)

​ 如果要使用 本 身 , 用 本身,用 $来代替

创建变量的目的:

用来代替一个文本字符串:

​ 1.系列文件的名字

​ 2. 传递给编译器的参数

​ 3. 需要运行的程序

​ 4. 需要查找源代码的目录

​ 5. 你需要输出信息的目录

​ 6. 你想做的其它事情。

预定义变量

​ AR 库文件维护程序的名称,默认为ar

​ AS 汇编程序名称,默认为as

​ CC C编译器的名称,默认为cc

​ CPP C预编译器的名称,默认为$(CC) -E

​ CXX C++编译器的名称,默认为g++

RM 文件删除程序的名称, 默认为rm -f

​ CFLAGS C编译器的选项,没有默认值

​ CXXFLAGS C++编译器的选项,没有默认值

​ 可以对预定义变量重新赋值

​ CC=gcc

​ CFLAGS=-Wall -O2

​ 自动变量

$* 不包含扩展名的目标文件名称

$+ 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件

$< 第一个依赖文件的名称

$? 所有时间戳比目标文件晚的的依赖文件,并以空格分开

$@ 目标文件的完整名称

$^ 所有不重复的目标依赖文件,以空格分开

$% 如果目标是归档成员,则该变量表示目标的归档成员名称

运行make 的选项

-C dir读入指定目录下的Makefile

-f file读入当前目录下的file文件作为Makefile

-I 忽略所有的命令执行错误

-I dir指定被包含的Makefile所在目录

-n 只打印要执行的命令,但不执行这些命令

-p 显示make变量数据库和隐含规则

-s 在执行命令时不显示命令

-w 如果make在执行过程中改变目录,打印当前目录名

​ makefile隐含规则

\1. 编译C程序隐含规则

xx.o 目标文件的依赖会自动的推导为xx.c

生成命令:

​ $(CC) -c $(CPPFLAGS) -o $@ $^

​ 示例:

​ test:test.o

​ gcc test.o -o test

​ test.o:

  1. 链接目标的隐含规则

xx 目标文件依赖与xx.o,通过运行C编译器来运行链接程序生成

生成指令:

​ $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

​ 示例:

​ test:

​ test.o: test.c

​ gcc -c test.c -o test.o

​ 隐含规则1,2可以一起使用

​ 示例:

​ test:

​ 特别注意:这个规则对于一个源文件和目标文件名同名的情况是适用

​ 即有一个源文件是xx.c 目标一定是xx

GDB调试工具

​ 使用gdb调试必须在gcc编译时加上 ‘-g’ 选项,告诉gcc在编译程序是加上调试信息

​ 启动gdb调试

  1. gdb [-q] 可执行程序

此时可以加 -q的选项取消调试器的版本信息等内容

  1. 先gdb [-q]

然后 file 可执行程序

​ 退出

​ q

gdb调试流程

​ 查看文件

​ l(list) 查看源码

​ l n 查看第n行的上下文

​ l func 查看func函数上下文

shell clear 执行清屏指令

​ 设置断点

​ b(break) n 在第n行设置断点

​ 查看断点情况

​ info b(break)

​ Num Type Disp Enb Address What

1 breakpoint keep y 0x0000555555554643 in main at test.c:10

​ breakpoint already hit 1 time

2 breakpoint keep y 0x0000555555554663 in main at test.c:13

​ Num:断点编号

​ Type:断点类型

​ Disp:断点在执行一次后是否失去作用,dis-是,keep-否

​ Enb:当前断点是否有效,y-是,n-否

​ Address:当前断点所在内存地址

​ What:断点在文件位置

​ 删除断点

  1. 使得断点失效

disable n 使第n个断点失效

enable n 使第n个断点恢复

  1. 直接删除断点

clear n 删除第n行的断点

delete n 删除第n个断点

delete 删除所有断点

运行程序

​ r(run) 运行程序

从断点处继续运行

​ c(continue)

​ 单步运行

​ n(next)

​ s(step)

查看变量值

​ p(print) n 打印变量名或表达式n

​ whatis n 变量名或表达式n的数据类型

​ p(print) 变量=值 给对应变量设置一个值

注意:

  1. 只有代码在运行或暂停状态才能查看变量的值
  2. 设置断点后程序在指定行之前停止

C语言库函数

函数:srand、rand

头文件:#include <stdlib.h>

函数原型: int rand(void);

void srand(unsigned int seed);

功能:srand设置随机数种子,一个整数值,rand生成随机数

特点:srand的随机数种子数值不变,那么生成的随机数值亦不会有任何变化

生成范围为0~RAND_MAX,如果没用随机种子,默认设置随机数种子为1

函数:time

头文件:#include <time.h>

函数原型:time_t time(time_t *tloc);

功能:获得系统时间作为随机数种子

srand((unsigned int)time(NULL));

函数:sleep

头文件: #include <unistd.h>

函数原型: unsigned int sleep(unsigned int seconds);

功能:使当前程序暂停/等待/休眠 一段时间再继续执行

参数:s 单位为秒

设置生成指定范围随机数

[0, max) rand()%max

(0, max] rand()%max+1

[0, max] rand()%(max+1)

[x, y) rand()%(y-x)+x

(x, y] rand()%(y-x)+x+1

[x, y] rand()%(y-x+1)+x

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值