C语言 -- 零基础入门详解

文章目录

引言

C语言,自从其诞生于1972年,一直是编程世界中不可或缺的一部分。由Dennis Ritchie和Ken Thompson在贝尔实验室创建,C语言凭借其强大的功能和灵活性,迅速成为了操作系统、嵌入式系统,甚至是其他编程语言的开发基石。如果你对计算机编程有浓厚的兴趣,那么掌握C语言几乎是一项必备技能。

C语言应用领域:

  • C语言可以写网站后台程序
  • C语言可以专门针对某个主题写出功能强大的程序库
  • C语言可以写出大型游戏的引擎
  • C语言可以写出另一个语言来
  • C语言可以写操作系统和驱动程序,并且只能用C语言编写
  • 任何设备只要配置了微处理器,就都支持C语言。从微波炉到手机,都是由C语言技术来推动的

1. 第一个C语言程序:HelloWorld

1.1 编写C语言代码:hello.c

#include <stdio.h>

int main()
{
    
    
	//这是第一个C语言代码 
	printf("hello world\n");
	return 0;
}

注意: C语言的源代码文件是一个普通的文本文件,但扩展名必须是.c。

1.2 代码分析

  1. include头文件包含
  • #include的意思是头文件包含,#include <stdio.h>代表包含stdio.h这个头文件
  • 使用C语言库函数需要提前包含库函数对应的头文件,如这里使用了printf()函数,需要包含stdio.h头文件
  1. main函数
  • 一个完整的C语言程序,是由一个、且只能有一个main()函数(又称主函数,必须有)和若干个其他函数结合而成(可选)。
  • main函数是C语言程序的入口,程序是从main函数开始执行。
  1. {} 括号,程序体和代码块
  • {}叫代码块,一个代码块内部可以有一条或者多条语句
  • C语言每句可执行代码都是";"分号结尾
  • 所有的#开头的行,都代表预编译指令,预编译指令行结尾是没有分号的
  • 所有的可执行语句必须是在代码块里面
  1. 注释
  • //叫行注释,注释的内容编译器是忽略的,注释主要的作用是在代码中加一些说明和解释,这样有利于代码的阅读
  • /**/叫块注释
  • 块注释是C语言标准的注释方法
  • 行注释是从C++语言借鉴过来的
  1. printf函数
  • printf是C语言库函数,功能是向标准输出设备输出一个字符串
  • printf(“hello world\n”);// \n的意思是回车换行
  1. return语句
  • return代表函数执行完毕,返回return代表函数的终止
  • 如果main定义的时候前面是int,那么return后面就需要写一个整数;如果main定义的时候前面是void,那么return后面什么也不需要写
  • 在main函数中return 0代表程序执行成功,return -1代表程序执行失败
  • int main()和void main()在C语言中是一样的,但C++只接受int main这种定义方式

2. 数据类型

2.1 常量与变量

2.1.1 关键字

在这里插入图片描述

2.1.2 数据类型

在这里插入图片描述

2.1.3 常量

  • 在程序运行过程中,其值不能被改变的量
  • 常量一般出现在表达式或赋值语句中

在这里插入图片描述

2.1.4 变量

  • 在程序运行过程中,其值可以改变
  • 变量在使用前必须先定义,定义变量前必须有相应的数据类型

标识符命名规则

  • 标识符不能是关键字
  • 标识符只能由字母、数字、下划线组成
  • 第一个字符必须为字母或下划线
  • 标识符中字母区分大小写

变量特点

  • 变量在编译时为其分配相应的内存空间
  • 可以通过其名字和地址访问相应内存
    在这里插入图片描述

声明和定义区别

  • 声明变量不需要建立存储空间,如:extern int a;
  • 定义变量需要建立存储空间,如:int b;
#include <stdio.h>

int main()
{
    
    
	//extern 关键字只做声明,不能做任何定义,后面还会学习,这里先了解
	//声明一个变量a,a在这里没有建立存储空间
	extern int a;
	a = 10;	//err, 没有空间,就不可以赋值

	int b = 10;	//定义一个变量b,b的类型为int,b赋值为10

	return 0;
}

从广义的角度来讲声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义:

2.1.5 使用示例

#include <stdio.h>
#define MAX 10 //声明了一个常量,名字叫MAX,值是10,常量的值一旦初始化不可改

int main()
{
    
    
	int a;	//定义了一个变量,其类型为int,名字叫a

	const int b = 10; //定义一个const常量,名为叫b,值为10
	//b = 11; //err,常量的值不能改变

	//MAX = 100;	//err,常量的值不能改变

	a = MAX;//将abc的值设置为MAX的值
	a = 123;

	printf("%d\n", a); //打印变量a的值

	return 0;
}

2.2 整型:int

2.2.1 整型变量的定义和输出

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	int a = 123;	//定义变量a,以10进制方式赋值为123
	int b = 0567;	//定义变量b,以8进制方式赋值为0567
	int c = 0xabc;	//定义变量c,以16进制方式赋值为0xabc

	printf("a = %d\n", a);
	printf("8进制:b = %o\n", b);
	printf("10进制:b = %d\n", b);
	printf("16进制:c = %x\n", c);
	printf("16进制:c = %X\n", c);
	printf("10进制:c = %d\n", c);

	unsigned int d = 0xffffffff; //定义无符号int变量d,以16进制方式赋值
	printf("有符号方式打印:d = %d\n", d);
	printf("无符号方式打印:d = %u\n", d);
	return 0;
}

2.2.2 整型变量的输入

#include <stdio.h>

int main()
{
    
    
	int a;
	printf("请输入a的值:");

	//不要加“\n”
	scanf("%d", &a);

	printf("a = %d\n", a); //打印a的值

	return 0;
}

2.2.3 short、int、long、long long

在这里插入图片描述

2.2.4 有符号数和无符号数区别

  1. 有符号数
    有符号数是最高位为符号位,0代表正数,1代表负数。
#include <stdio.h>

int main()
{
    
    
	signed int a = -1089474374; //定义有符号整型变量a
	printf("%X\n", a); //结果为 BF0FF0BA

	//B       F      0        F       F     0        B	      A
	//1011 1111 0000 1111 1111 0000 1011 1010

	return 0;
}
  1. 无符号数
    无符号数最高位不是符号位,而就是数的一部分,无符号数不可能是负数。
#include <stdio.h>

int main()
{
    
    
	unsigned int a = 3236958022; //定义无符号整型变量a
	printf("%X\n", a); //结果为 C0F00F46

	return 0;
}
  1. 有符号和无符号整型取值范围
    在这里插入图片描述

2.3 sizeof关键字

  • sizeof不是函数,所以不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节
#include <stdio.h>

int main()
{
    
    
	int a;
	int b = sizeof(a);//sizeof得到指定值占用内存的大小,单位:字节
	printf("b = %d\n", b);

	size_t c = sizeof(a);
	printf("c = %u\n", c);//用无符号数的方式输出c的值

	return 0;
}

2.4 字符型:char

2.4.1 字符变量的定义和输出

字符型变量用于存储一个单一字符,在 C 语言中用 char 表示,其中每个字符变量都会占用 1 个字节。在给字符型变量赋值时,需要用一对英文半角格式的单引号(’ ')把字符括起来。

字符变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的 ASCII 编码放到变量的存储单元中。char的本质就是一个1字节大小的整型。

#include <stdio.h>

int main()
{
    
    
	char ch = 'a';
	printf("sizeof(ch) = %u\n", sizeof(ch));

	printf("ch[%%c] = %c\n", ch); //打印字符
	printf("ch[%%d] = %d\n", ch); //打印‘a’ ASCII的值

	char A = 'A';
	char a = 'a';
	printf("a = %d\n", a);		//97
	printf("A = %d\n", A);	//65

	printf("A = %c\n", 'a' - 32); //小写a转大写A
	printf("a = %c\n", 'A' + 32); //大写A转小写a

	ch = ' ';
	printf("空字符:%d\n", ch); //空字符ASCII的值为32
	printf("A = %c\n", 'a' - ' '); //小写a转大写A
	printf("a = %c\n", 'A' + ' '); //大写A转小写a

	return 0;
}

2.4.2 字符变量的输入

#include <stdio.h>

int main()
{
    
    
	char ch;
	printf("请输入ch的值:");

	//不要加“\n”
	scanf("%c", &ch);
	printf("ch = %c\n", ch); //打印ch的字符

	return 0;
}

2.4.3 ASCII对照表

在这里插入图片描述

2.4.4 转义字符

在这里插入图片描述

注意:红色字体标注的为不可打印字符。

#include <stdio.h>

int main()
{
    
    
	printf("abc");
	printf("\refg\n"); //\r切换到句首, \n为换行键

	printf("abc");
	printf("\befg\n");//\b为退格键, \n为换行键

	printf("%d\n", '\123');// '\123'为8进制转义字符,0123对应10进制数为83
	printf("%d\n", '\x23');// '\x23'为16进制转义字符,0x23对应10进制数为35

	return 0;
}

2.5 实型(浮点型):float、double

实型变量也可以称为浮点型变量,浮点型变量是用来存储小数数值的。在C语言中, 浮点型变量分为两种: 单精度浮点数(float)、 双精度浮点数(double), 但是double型变量所表示的浮点数比 float 型变量更精确。

由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。

不以f结尾的常量是double类型,以f结尾的常量(如3.14f)是float类型。

#include <stdio.h>

int main()
{
    
    
	//传统方式赋值
	float a = 3.14f; //或3.14F
	double b = 3.14;

	printf("a = %f\n", a);
	printf("b = %lf\n", b);

	//科学法赋值
	a = 3.2e3f; //3.2*1000 = 3200,e可以写E
	printf("a1 = %f\n", a);

	a = 100e-3f; //100*0.001 = 0.1
	printf("a2 = %f\n", a);

	a = 3.1415926f;
	printf("a3 = %f\n", a); //结果为3.141593

	return 0;
}

2.6 C语言如何表示相应进制数

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	int a = 123;		//十进制方式赋值
	int b = 0123;		//八进制方式赋值, 以数字0开头
	int c = 0xABC;	//十六进制方式赋值

	//如果在printf中输出一个十进制数那么用%d,八进制用%o,十六进制是%x
	printf("十进制:%d\n",a );
	printf("八进制:%o\n", b);	//%o,为字母o,不是数字
	printf("十六进制:%x\n", c);

	return 0;
}

2.7 类型限定符

在这里插入图片描述

2.8 printf函数和putchar函数

printf是输出一个字符串,putchar输出一个char。
printf格式字符:
在这里插入图片描述

#include <stdio.h>
int main()
{
    
    
	int a = 100;
	printf("a = %d\n", a);//格式化输出一个字符串
	printf("%p\n", &a);//输出变量a在内存中的地址编号
	printf("%%d\n");

	char c = 'a';
	putchar(c);//putchar只有一个参数,就是要输出的char
	long a2 = 100;
	printf("%ld, %lx, %lo\n", a2, a2, a2);

	long long a3 = 1000;
	printf("%lld, %llx, %llo\n", a3, a3, a3);

	int abc = 10;
	printf("abc = '%6d'\n", abc);
	printf("abc = '%-6d'\n", abc);
	printf("abc = '%06d'\n", abc);
	printf("abc = '%-06d'\n", abc);

	double d = 12.3;
	printf("d = \' %-10.3lf \'\n", d);

	return 0;
}

2.9 scanf函数与getchar函数

  • getchar是从标准输入设备读取一个char。
  • scanf通过%转义的方式可以得到用户通过标准输入设备输入的数据。
#include <stdio.h>

int main()
{
    
    
	char ch1;
	char ch2;
	char ch3;
	int a;
	int b;

	printf("请输入ch1的字符:");
	ch1 = getchar();
	printf("ch1 = %c\n", ch1);

	getchar(); //测试此处getchar()的作用

	printf("请输入ch2的字符:");
	ch2 = getchar();
	printf("\'ch2 = %ctest\'\n", ch2);

	getchar(); //测试此处getchar()的作用
	printf("请输入ch3的字符:");
	scanf("%c", &ch3);//这里第二个参数一定是变量的地址,而不是变量名
	printf("ch3 = %c\n", ch3);

	printf("请输入a的值:");
	scanf("%d", &a);
	printf("a = %d\n", a);

	printf("请输入b的值:");
	scanf("%d", &b);
	printf("b = %d\n", b);

	return 0;
}

3. 运算符与表达式

3.1 常用运算符分类

在这里插入图片描述

3.2 算术运算符

在这里插入图片描述

3.3 赋值运算符

在这里插入图片描述

3.4 比较运算符

在这里插入图片描述

C 语言的比较运算中, “真”用数字“1”来表示, “假”用数字“0”来表示。

3.5 逻辑运算符

在这里插入图片描述

3.6 类型转换

数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题。

转换的方法有两种:

  • 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成
  • 强制类型转换:把表达式的运算结果强制转换成所需的数据类型

类型转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低。在这里插入图片描述

3.6.1 隐式转换

#include <stdio.h>

int main()
{
    
    
	int num = 5;
	printf("s1=%d\n", num / 2);
	printf("s2=%lf\n", num / 2.0);

	return 0;
}

3.6.2 强制转换

强制类型转换指的是使用强制类型转换运算符,将一个变量或表达式转化成所需的类型,其基本语法格式如下所示:
(类型说明符) (表达式)

#include <stdio.h>

int main()
{
    
    
	float x = 0;
	int i = 0;
	x = 3.6f;

	i = x;			//x为实型, i为整型,直接赋值会有警告
	i = (int)x;		//使用强制类型转换

	printf("x=%f, i=%d\n", x, i);

	return 0;
}

4. 程序流程结构

4.1 概述

C语言支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构。

  • 顺序结构:程序按顺序执行,不发生跳转
  • 选择结构:依据是否满足条件,有选择的执行相应功能
  • 循环结构:依据条件是否满足,循环多次执行某段代码

4.2 选择结构

4.2.1 if语句

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	int a = 1;
	int b = 2;

	if (a > b)
	{
    
    
		printf("%d\n", a);
	}

	return 0;
} 

4.2.2 if…else语句

在这里插入图片描述

#include <stdio.h>
int main()
{
    
    
	int a = 1;
	int b = 2;

	if (a > b)
	{
    
    
		printf("%d\n", a);
	}
	else
	{
    
    
		printf("%d\n", b);
	}
	return 0;
}

4.2.3 if…else if…else语句

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	unsigned int a;
	scanf("%u", &a);

	if (a < 10)
	{
    
    
		printf("个位\n");
	}
	else if (a < 100)
	{
    
    
		printf("十位\n");
	}
	else if (a < 1000)
	{
    
    
		printf("百位\n");
	}
	else
	{
    
    
		printf("很大\n");
	}

	return 0;
}

4.2.4 三目运算符

#include <stdio.h>

int main()
{
    
    
	int a = 10;
	int b = 20;
	int c;

	if (a > b)
	{
    
    
		c = a;
	}
	else
	{
    
    
		c = b;
	}
	printf("c1 = %d\n", c);

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

	return 0;
}

4.2.5 switch语句

#include <stdio.h>

int main()
{
    
    
	char c;
	c = getchar();

	switch (c) //参数只能是整型变量
	{
    
    
	case '1':
		printf("OK\n");
		break;//switch遇到break就中断了
	case '2':
		printf("not OK\n");
		break;
	default://如果上面的条件都不满足,那么执行default
		printf("are u ok?\n");
	}
	return 0;
}

4.3 循环结构

4.3.1 while语句

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	int a = 20;
	while (a > 10)
	{
    
    
		scanf("%d", &a);
		printf("a = %d\n", a);
	}

	return 0;
}

4.3.2 do…while语句

在这里插入图片描述

#include <stdio.h>

int main()
{
    
    
	int a = 1;
	do
	{
    
    
		a++;
		printf("a = %d\n", a);
	} while (a < 10);

	return 0;
}

4.3.3 for语句

#include <stdio.h>

int main()
{
    
    
	int i;
	int sum = 0;
	for (i = 0; i <= 100; i++)
	{
    
    
		sum += i;

	}

	printf("sum = %d\n", sum);

	return 0;
}

4.3.4 嵌套循环

循环语句之间可以相互嵌套

#include <stdio.h>

int main()
{
    
    
	int num = 0;
	int i, j, k;
	for (i = 0; i < 10; i++)
	{
    
    
		for (j = 0; j < 10; j++)
		{
    
    
			for (k = 0; k < 10; k++)
			{
    
    
				printf("hello world\n");
				num++;
			}
		}
	}

	printf("num = %d\n", num);

	return 0;
}

4.4 跳转语句break、continue、goto

4.4.1 break语句

在switch条件语句和循环语句中都可以使用break语句:

  • 当它出现在switch条件语句中时,作用是终止某个case并跳出switch结构。
  • 当它出现在循环语句中,作用是跳出当前内循环语句,执行后面的代码。
  • 当它出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的代码。
#include <stdio.h>

int main()
{
    
    
	int i = 0;
	while (1)
	{
    
    
		i++;
		printf("i = %d\n", i);

		if (i == 10)
		{
    
    
			break; //跳出while循环
		}
	}

	int flag = 0;
	int m = 0;
	int n = 0;

	for (m = 0; m < 10; m++)
	{
    
    
		for (n = 0; n < 10; n++)
		{
    
    
			if (n == 5)
			{
    
    
				flag = 1;
				break; //跳出for (n = 0; n < 10; n++)
			}
		}

		if (flag == 1)
		{
    
    
			break; //跳出for (m = 0; m < 10; m++)
		}
	}

	return 0;
}

4.4.2 continue语句

在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时就需要使用continue语句。

#include<stdio.h>

int main()
{
    
    
	int sum = 0;           //定义变量sum

	for (int i = 1; i <= 100; i++)
	{
    
    
		if (i % 2 == 0)   //如果i是一个偶数,执行if语句中的代码
		{
    
    
			continue;      //结束本次循环
		}
		sum += i;          //实现sum和i的累加
	}

	printf("sum = %d\n", sum);

	return 0;
}

4.4.3 goto语句(无条件跳转,尽量少用)

#include <stdio.h>

int main()
{
    
    
	goto End; //无条件跳转到End的标识
	printf("aaaaaaaaa\n");

End:
	printf("bbbbbbbb\n");

	return 0;
}

5. 数组和字符串

5.1 概述

在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组。

数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
在这里插入图片描述

数组属于构造数据类型:

  • 一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型。
  • 按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别。

通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,我们将二维及以上的数组称为多维数组。

5.2 一维数组

5.2.1 一维数组的定义和使用

  • 数组名字符合标识符的书写规定(数字、英文字母、下划线)
  • 数组名不能与其它变量名相同,同一作用域内是唯一的
  • 方括号[]中常量表达式表示数组元素的个数
  • 定义数组时[]内最好是常量,使用数组时[]内即可是常量,也可以是变量
#include <stdio.h>

int main()
{
    
    
	int a[10];//定义了一个数组,名字叫a,有10个成员,每个成员都是int类型
	//a[0]…… a[9],没有a[10]
	//没有a这个变量,a是数组的名字,但不是变量名,它是常量
	a[0] = 0;
	//……
	a[9] = 9;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		a[i] = i; //给数组赋值
	}

	//遍历数组,并输出每个成员的值
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

5.2.2 一维数组的初始化

在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。

		int a[10] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
		int a[10] = {
    
     1, 2, 3 };//初始化前三个成员,后面所有元素都设置为0
		int a[10] = {
    
     0 };//所有的成员都设置为0
	
		 //[]中不定义元素个数,定义时必须初始化
	    int a[] = {
    
     1, 2, 3, 4, 5 };//定义了一个数组,有5个成员

5.2.3 数组名

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

#include <stdio.h>

int main()
{
    
    
	int a[10] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量

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

	int n = sizeof(a); //数组占用内存的大小,10个int类型,10 * 4  = 40
	int n0 = sizeof(a[0]);//数组第0个元素占用内存大小,第0个元素为int,4

	int i = 0;
	for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
    
    
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

5.3 二维数组

5.3.1 二维数组的定义和使用

二维数组定义的一般形式是:

类型说明符 数组名[常量表达式1][常量表达式2]
其中常量表达式1表示第一维下标的长度,常量表达式2 表示第二维下标的长度。
int a[3][4];

  • 命名规则同一维数组
  • 定义了一个三行四列的数组,数组名为a其元素类型为整型,该数组的元素个数为3×4个,即:

在这里插入图片描述

二维数组a是按行进行存放的,先存放a[0]行,再存放a[1]行、a[2]行,并且每行有四个元素,也是依次存放的。

  • 二维数组在概念上是二维的:其下标在两个方向上变化,对其访问一般需要两个下标。
  • 在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,也就是说内存中只有一维数组,即放完一行之后顺次放入第二行,和一维数组存放方式是一样的
#include <stdio.h>

int main()
{
    
    
	//定义了一个二维数组,名字叫a
	//由3个一维数组组成,这个一维数组是int [4]
	//这3个一维数组的数组名分别为a[0],a[1],a[2]
	int a[3][4];

	a[0][0] = 0;
	//……
	a[2][3] = 12;

	//给数组每个元素赋值
	int i = 0;
	int j = 0;
	int num = 0;
	for (i = 0; i < 3; i++)
	{
    
    
		for (j = 0; j < 4; j++)
		{
    
    
			a[i][j] = num++;
		}
	}

	//遍历数组,并输出每个成员的值
	for (i = 0; i < 3; i++)
	{
    
    
		for (j = 0; j < 4; j++)
		{
    
    
			printf("%d, ", a[i][j]);
		}
		printf("\n");
	}

	return 0;
}

5.3.2 二维数组的初始化

//分段赋值 	int a[3][4] = {
    
    { 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
	int a[3][4] = 
	{
    
     
		{
    
     1, 2, 3, 4 },
		{
    
     5, 6, 7, 8, },
		{
    
     9, 10, 11, 12 }
	};

	//连续赋值
	int a[3][4] = {
    
     1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12  };

	//可以只给部分元素赋初值,未初始化则为0
	int a[3][4] = {
    
     1, 2, 3, 4  };

	//所有的成员都设置为0
	int a[3][4] = {
    
    0};

	//[]中不定义元素个数,定义时必须初始化
	int a[][4] = {
    
     1, 2, 3, 4, 5, 6, 7, 8};

5.3.3 数组名

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

#include <stdio.h>

int main()
{
    
    
	//定义了一个二维数组,名字叫a
	//二维数组是本质上还是一维数组,此一维数组有3个元素
//每个元素又是一个一维数组int[4]
	int a[3][4] = {
    
     1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12  };

	//数组名为数组首元素地址,二维数组的第0个元素为一维数组
	//第0个一维数组的数组名为a[0]
	printf("a = %p\n", a);
	printf("a[0] = %p\n", a[0]);
	
	//测二维数组所占内存空间,有3个一维数组,每个一维数组的空间为4*4
	//sizeof(a) = 3 * 4 * 4 = 48
	printf("sizeof(a) = %d\n", sizeof(a));

	//测第0个元素所占内存空间,a[0]为第0个一维数组int[4]的数组名,4*4=16
	printf("sizeof(a[0]) = %d\n", sizeof(a[0]) );

	//测第0行0列元素所占内存空间,第0行0列元素为一个int类型,4字节
	printf("sizeof(a[0][0]) = %d\n", sizeof(a[0][0]));

	//求二维数组行数
	printf("i = %d\n", sizeof(a) / sizeof(a[0]));

	// 求二维数组列数
	printf("j = %d\n", sizeof(a[0]) / sizeof(a[0][0]));

	//求二维数组行*列总数
	printf("n = %d\n", sizeof(a) / sizeof(a[0][0]));

	return 0;
}

5.4 多维数组(了解)

多维数组的定义与二维数组类似,其语法格式具体如下:

数组类型修饰符 数组名 [n1][n2]…[nn];
int a[3][4][5];

定义了一个三维数组,数组的名字是a,数组的长度为3,每个数组的元素又是一个二维数组,这个二维数组的长度是4,并且这个二维数组中的每个元素又是一个一维数组,这个一维数组的长度是5,元素类型是int。

#include <stdio.h>

int main()
{
    
    
	//int a[3][4][5] ;//定义了一个三维数组,有3个二维数组int[4][5]
	int a[3][4][5] = {
    
     {
    
     {
    
     1, 2, 3, 4, 5 }, {
    
     6, 7, 8, 9, 10 }, {
    
     0 }, {
    
     0 } }, {
    
     {
    
     0 }, {
    
     0 }, {
    
     0 }, {
    
     0 } }, {
    
     {
    
     0 }, {
    
     0 }, {
    
     0 }, {
    
     0 } } };

	int i, j, k;
	for (i = 0; i < 3; i++)
	{
    
    
		for (j = 0; j < 4; j++)
		{
    
    
			for (k = 0; k < 5; k++)
			{
    
    
				//添加访问元素代码
				printf("%d, ", a[i][j][k]);
			}
			printf("\n");
		}
	}
	return 0;
}

5.5 字符数组与字符串

5.5.1 字符数组与字符串区别

  • C语言中没有字符串这种数据类型,可以通过char的数组来替代;
  • 字符串一定是一个char的数组,但char的数组未必是字符串;
  • 数字0(和字符‘\0’等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组
#include <stdio.h>

int main()
{
    
    
	char c1[] = {
    
     'c', ' ', 'p', 'r', 'o', 'g' }; //普通字符数组
	printf("c1 = %s\n", c1); //乱码,因为没有’\0’结束符

	//以‘\0’(‘\0’就是数字0)结尾的字符数组是字符串
	char c2[] = {
    
     'c', ' ', 'p', 'r', 'o', 'g', '\0'}; 
	printf("c2 = %s\n", c2);

	//字符串处理以‘\0’(数字0)作为结束符,后面的'h', 'l', 'l', 'e', 'o'不会输出
	char c3[] = {
    
     'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'l', 'l', 'e', 'o', '\0'};
	printf("c3 = %s\n", c3);

	return 0;
}

5.5.2 字符串的初始化

#include <stdio.h>

// C语言没有字符串类型,通过字符数组模拟
// C语言字符串,以字符‘\0’, 数字0
int main()
{
    
    
	//不指定长度, 没有0结束符,有多少个元素就有多长
	char buf[] = {
    
     'a', 'b', 'c' };
	printf("buf = %s\n", buf);	//乱码

	//指定长度,后面没有赋值的元素,自动补0
	char buf2[100] = {
    
     'a', 'b', 'c' };
char buf[1000]={
    
    “hello”};
	printf("buf2 = %s\n", buf2);

	//所有元素赋值为0
	char buf3[100] = {
    
     0 };

	//char buf4[2] = { '1', '2', '3' };//数组越界

	char buf5[50] = {
    
     '1', 'a', 'b', '0', '7' };
	printf("buf5 = %s\n", buf5);

	char buf6[50] = {
    
     '1', 'a', 'b', 0, '7' };
	printf("buf6 = %s\n", buf6);

	char buf7[50] = {
    
     '1', 'a', 'b', '\0', '7' };
	printf("buf7 = %s\n", buf7);

	//使用字符串初始化,编译器自动在后面补0,常用
	char buf8[] = "agjdslgjlsdjg";

	//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
	//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
	// \012相当于\n
	char str[] = "\012abc";
	printf("str == %s\n", str);

	return 0;
}

5.5.3 字符串的输入输出

由于字符串采用了’\0’标志,字符串的输入输出将变得简单方便。

#include <stdio.h>

int main()
{
    
    
	char str[100];
   
	printf("input string1 : \n");
	scanf("%s", str);//scanf(“%s”,str)默认以空格分隔
	printf("output:%s\n", str);

	return 0;
}

6. 函数

6.1 概述

6.1.1 函数分类

C 程序是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。

从函数定义的角度看,函数可分为系统函数和用户定义函数两种:

  • 系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
  • 用户定义函数:用以解决用户的专门需要。

6.1.2 函数的作用

  • 函数的使用可以省去重复代码的编写,降低代码重复率
// 求两数的最大值
int max(int a, int b)
{
    
    
	if (a > b){
    
    
		return a;
	}
	else{
    
    
		return b;
	}
}

int main()
{
    
    
	// 操作1 ……
	// ……
	int a1 = 10, b1 = 20, c1 = 0;
	c1 = max(a1, b1); // 调用max()

	// 操作2 ……
	// ……
	int a2 = 11, b2 = 21, c2 = 0;
	c2 = max(a2, b2); // 调用max()

	// ……

	return 0;
}
  • 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善

假如我们编写一个实现以下功能的程序:读入一行数字;对数字进行排序;找到它们的平均值;打印出一个柱状图。如果我们把这些操作直接写在main()里,这样可能会给用户感觉代码会有点凌乱。但,假如我们使用函数,这样可以让程序更加清晰、模块化:

#include <stdio.h>

int main()
{
    
    
	float list[50];

	// 这里只是举例,函数还没有实现
	readlist(list, 50);
	sort(list, 50);
	average(list, 50);
	bargraph(list, 50);

	return 0;
}

这里我们可以这么理解,程序就像公司,公司是由部门组成的,这个部门就类似于C程序的函数。默认情况下,公司就是一个大部门( 只有一个部门的情况下 ),相当于C程序的main()函数。如果公司比较小( 程序比较小 ),因为任务少而简单,一个部门即可( main()函数 )胜任。但是,如果这个公司很大( 大型应用程序 ),任务多而杂,如果只是一个部门管理( 相当于没有部门,没有分工 ),我们可想而知,公司管理、运营起来会有多混乱,不是说这样不可以运营,只是这样不完美而已,如果根据公司要求分成一个个部门( 根据功能封装一个一个函数 ),招聘由行政部门负责,研发由技术部门负责等,这样就可以分工明确,结构清晰,方便管理,各部门之间还可以相互协调。

6.1.3 函数的调用:产生随机数

当调用函数时,需要关心5要素:

  • 头文件:包含指定的头文件
  • 函数名字:函数名字必须和头文件声明的名字一样
  • 功能:需要知道此函数能干嘛后才调用
  • 参数:参数类型要匹配
  • 返回值:根据需要接收返回值
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main()
{
    
    
	time_t tm = time(NULL);//得到系统时间
	srand((unsigned int)tm);//随机种子只需要设置一次即可

	int r = rand();
	printf("r = %d\n", r);

	return 0;

}

6.2 函数的定义

6.2.1 函数定义格式

函数定义的一般形式:

返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
在这里插入图片描述

6.2.2 函数名字、形参、函数体、返回值

  1. 函数名
    理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号(),代表这个为函数,不是普通的变量名。
  2. 形参列表
    在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值。
void max(int a = 10, int b = 20) // error, 形参不能赋值
{
    
    
}

在定义函数时指定的形参,必须是,类型+变量的形式:

//1: right, 类型+变量
void max(int a, int b)
{
    
    
}

//2: error, 只有类型,没有变量
void max(int, int)
{
    
    
}

//3: error, 只有变量,没有类型
int a, int b;
void max(a, b)
{
    
    
}

在定义函数时指定的形参,可有可无,根据函数的需要来设计,如果没有形参,圆括号内容为空,或写一个void关键字:

// 没形参, 圆括号内容为空
void max()
{
    
    
}

// 没形参, 圆括号内容为void关键字
void max(void)
{
    
    
}
  1. 函数体
    花括号{ }里的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这些写到别的函数里。
  2. 返回值
    函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式。
    如果函数带返回值,return后面必须跟着一个值,如果函数没有返回值,函数名字的前面必须写一个void关键字,这时候,我们写代码时也可以通过return中断函数(也可以不用),只是这时,return后面不带内容( 分号“;”除外)。
void max()// 最好要有void关键字
{
    
    
	return; // 中断函数,这个可有可无
}

6.3 函数的调用

定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个 C 程序里有且只有一个main()函数。

6.3.1 函数执行流程

#include <stdio.h>

void print_test()
{
    
    
	printf("this is for test\n");
}

int main()
{
    
    
	print_test();	// print_test函数的调用

	return 0;
}
  1. 进入main()函数
  2. 调用print_test()函数:
  • 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
  • 如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有形参,参数类型匹配;
  • 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
  1. print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。

6.3.2 函数的形参和实参

  • 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
  • 实参出现在主调函数中,进入被调函数后,实参也不能使用。
  • 实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。
  • 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放。
  • 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。

6.3.3 无参函数调用

如果是调用无参函数,则不能加上“实参”,但括号不能省略。

// 函数的定义
void test()
{
    
    
}

int main()
{
    
    
	// 函数的调用
	test();	// right, 圆括号()不能省略
	test(250); // error, 函数定义时没有参数

return 0;
}

6.3.4 有参函数调用

a) 如果实参表列包含多个实参,则各参数间用逗号隔开。

// 函数的定义
void test(int a, int b)
{
    
    
}

int main()
{
    
    
	int p = 10, q = 20;
	test(p, q);	// 函数的调用

	return 0;
}

b) 实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。

c) 实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号( )外面定义好、赋好值的变量。

// 函数的定义
void test(int a, int b)
{
    
    
}

int main()
{
    
    
	// 函数的调用
	int p = 10, q = 20;
	test(p, q);	// right
	test(11, 30 - 10); // right

	test(int a, int b); // error, 不应该在圆括号里定义变量

	return 0;
}

6.3.5 函数返回值

a)如果函数定义没有返回值,函数调用时不能写void关键字,调用函数时也不能接收函数的返回值。

// 函数的定义
void test()
{
    
    
}

int main()
{
    
    
	// 函数的调用
	test(); // right
	void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
	int a = test();	// error, 函数定义根本就没有返回值

	return 0;
}

b)如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收。

// 函数的定义, 返回值为int类型
int test()
{
    
    
}

int main()
{
    
    
	// 函数的调用
	int a = test(); // right, a为int类型
	int b;
	b = test();	// right, 和上面等级

	char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配

	// error, 必须定义一个匹配类型的变量来接收返回值
	// int只是类型,没有定义变量
	int = test();	
	
	// error, 必须定义一个匹配类型的变量来接收返回值
	// int只是类型,没有定义变量
	int test();
	
	return 0;
}

6.4 函数的声明

如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。

所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。

注意:一个函数只能被定义一次,但可以声明多次。

#include <stdio.h>

int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式

int main()
{
    
    
	int a = 10, b = 25, num_max = 0;
	num_max = max(a, b); // 函数的调用

	printf("num_max = %d\n", num_max);

	return 0;
}

// 函数的定义
int max(int x, int y)
{
    
    
	return x > y ? x : y;
}

函数定义和声明的区别:
1)定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
2)声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。

6.5 main函数与exit函数

在main函数中调用exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中调用exit,那么程序终止。

#include <stdio.h>
#include <stdlib.h>

void fun()
{
    
    
	printf("fun\n");
	//return;
	exit(0);
}

int main()
{
    
    
	fun();
	while (1);

	return 0;
}

6.6 多文件(分文件)编程

  • 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件
  • 在头文件对应的xxx.c中实现xxx.h声明的函数

在这里插入图片描述

6.6.1 防止头文件重复包含

当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。
在这里插入图片描述
在这里插入图片描述

为了避免同一个文件被include多次,C/C++中有两种方式,一种是 #ifndef 方式,一种是 #pragma once 方式。

7. 指针

7.1 概述

7.1.1 内存

内存含义:

  • 存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分
  • 内存:内部存贮器,暂存程序/数据——掉电丢失 SRAM、DRAM、DDR、DDR2、DDR3。
  • 外存:外部存储器,长时间保存程序/数据—掉电不丢ROM、ERRROM、FLASH(NAND、NOR)、硬盘、光盘。

内存是沟通CPU与硬盘的桥梁:

  • 暂存放CPU中的运算数据
  • 暂存与硬盘等外部存储器交换的数据

7.1.2 物理存储器和存储地址空间

有关内存的两个概念:物理存储器和存储地址空间。

物理存储器:实际存在的具体存储器芯片。

  • 主板上装插的内存条
  • 显示卡上的显示RAM芯片
  • 各种适配卡上的RAM芯片和ROM芯片

存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。

  • 编码:对每个物理存储单元(一个字节)分配一个号码
  • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

7.1.3 内存地址

  • 将内存抽象成一个很大的一维字符数组
  • 编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关)
  • 这个内存编号我们称之为内存地址,内存中的每一个数据都会分配相应的地址:
  • char:占一个字节分配一个地址
  • int: 占四个字节分配四个地址
  • float、struct、函数、数组等

在这里插入图片描述

7.1.4 指针和指针变量

  • 内存区的每一个字节都有一个编号,这就是“地址”。
  • 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
  • 指针的实质就是内存“地址”。指针就是地址,地址就是指针
  • 指针是内存单元的编号,指针变量是存放地址的变量。
  • 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。

在这里插入图片描述

7.2 指针基础知识

7.2.1 指针变量的定义和使用

  • 指针也是一种数据类型,指针变量也是一种变量
  • 指针变量指向谁,就把谁的地址赋值给指针变量
  • “*”操作符操作的是指针变量指向的内存空间
#include <stdio.h>

int main()
{
    
    
	int a = 0;
	char b = 100;
	printf("%p, %p\n", &a, &b); //打印a, b的地址

	//int *代表是一种数据类型,int*指针类型,p才是变量名
	//定义了一个指针类型的变量,可以指向一个int类型变量的地址
	int *p;
	p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
	printf("%d\n", *p);//p指向了a的地址,*p就是a的值

	char *p1 = &b;
	printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值

	return 0;
}

注意:&可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。

7.2.2 通过指针间接修改变量的值

	int a = 0;
	int b = 11;
	int *p = &a;

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

	p = &b;
	*p = 22;
	printf("b = %d, *p = %d\n", b, *p);

7.2.3 指针大小

  • 使用sizeof()测量指针的大小,得到的总是:4或8
  • sizeof()测的是指针变量指向存储地址的大小
  • 在32位平台,所有的指针(地址)都是32位(4字节)
  • 在64位平台,所有的指针(地址)都是64位(8字节)
	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(p4) = %d\n", sizeof(p4));
	printf("sizeof(double *) = %d\n", sizeof(double *));

7.2.4 野指针和空指针

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。

	int a = 100;
	int *p;
	p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义

	p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义

	*p = 1000;  //操作野指针指向未知区域,内存出问题,err

但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。

7.2.5 万能指针void *

void *指针可以指向任意变量的内存空间:

	void *p = NULL;

	int a = 10;
	p = (void *)&a; //指向变量时,最好转换为void *

	//使用指针变量指向的内存时,转换为int *
	*( (int *)p ) = 11;
	printf("a = %d\n", a);

7.2.6 const修饰的指针变量

	int a = 100;
	int b = 200;

//指向常量的指针
	//修饰*,指针指向内存区域不能修改,指针指向可以变
	const int * p1 = &a; //等价于int const *p1 = &a;
	//*p1 = 111; //err
	p1 = &b; //ok

//指针常量
	//修饰p1,指针指向不能变,指针指向的内存可以修改
	int * const p2 = &a;
	//p2 = &b; //err
	*p2 = 222; //ok

在编辑程序时,指针作为函数参数,如果不想修改指针对应内存空间的值,需要使用const修饰指针数据类型。

7.3 指针和数组

7.3.1 数组名

数组名字是数组的首元素地址,但它是一个常量

	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	printf("a = %p\n", a);
	printf("&a[0] = %p\n", &a[0]);

	//a = 10; //err, 数组名只是常量,不能修改

7.3.2 指针操作数组元素

#include <stdio.h>

int  main()
{
    
    
	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);
	
	for (i = 0; i < n; i++)
	{
    
    
		//printf("%d, ", a[i]);
		printf("%d, ", *(a+i));
	}
	printf("\n");

	int *p = a; //定义一个指针变量保存a的地址
	for (i = 0; i < n; i++)
	{
    
    
		p[i] = 2 * i;
	}

	for (i = 0; i < n; i++)
	{
    
    
		printf("%d, ", *(p + i));
	}
	printf("\n");


	return 0;
}

7.3.3 指针加减运算

  1. 加法运算
  • 指针计算不是简单的整数相加
  • 如果是一个int *,+1的结果是增加一个int的大小
  • 如果是一个char *,+1的结果是增加一个char大小
#include <stdio.h>

int main()
{
    
    
	int a;
	int *p = &a;
	printf("%d\n", p);
	p += 2;//移动了2个int
	printf("%d\n", p);

	char b = 0;
	char *p1 = &b;
	printf("%d\n", p1);
	p1 += 2;//移动了2个char
	printf("%d\n", p1);

	return 0;
}

通过改变指针指向操作数组元素:

#include <stdio.h>

int main()
{
    
    
	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);

	int *p = a;
	for (i = 0; i < n; i++)
	{
    
    
		printf("%d, ", *p);
		p++;
	}
	printf("\n");
	
	return 0;
}
  1. 减法运算
    示例1:
#include <stdio.h>

int main()
{
    
    
	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);

	int *p = a+n-1;
	for (i = 0; i < n; i++)
	{
    
    
		printf("%d, ", *p);
		p--;
	}
	printf("\n");

	return 0;
}

示例2:

#include <stdio.h>

int main()
{
    
    
	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int *p2 = &a[2]; //第2个元素地址
	int *p1 = &a[1]; //第1个元素地址
	printf("p1 = %p, p2 = %p\n", p1, p2);

	int n1 = p2 - p1; //n1 = 1
	int n2 = (int)p2 - (int)p1; //n2 = 4
	printf("n1 = %d, n2 = %d\n", n1, n2);
	
	return 0;
}

7.3.4 指针数组

指针数组,它是数组,数组的每个元素都是指针类型。

#include <stdio.h>

int main()
{
    
    
	//指针数组
	int *p[3];
	int a = 1;
	int b = 2;
	int c = 3;
	int i = 0;

	p[0] = &a;
	p[1] = &b;
	p[2] = &c;

	for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
	{
    
    
		printf("%d, ", *(p[i]));
	}
	printf("\n");
	
	return 0;
}

7.4 多级指针

  • C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
  • 二级指针就是指向一个一级指针变量地址的指针。
	int a = 10;
	int *p = &a; //一级指针
	*p = 100; //*p就是a

	int **q = &p;
	//*q就是p
	//**q就是a

	int ***t = &q;
	//*t就是q
	//**t就是p
	//***t就是a

7.5 指针和函数

7.5.1 函数形参改变实参的值

#include <stdio.h>

void swap1(int x, int y)
{
    
    
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
	printf("x = %d, y = %d\n", x, y);
}

void swap2(int *x, int *y)
{
    
    
	int tmp;
	tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
    
    
	int a = 3;
	int b = 5;
	swap1(a, b); //值传递
	printf("a = %d, b = %d\n", a, b);

	a = 3;
	b = 5;
	swap2(&a, &b); //地址传递
	printf("a2 = %d, b2 = %d\n", a, b);

	return 0;
}

7.5.2 数组名做函数参数

数组名做函数参数,函数的形参会退化为指针:

#include <stdio.h>

void printArrary(int *a, int n)
{
    
    
	int i = 0;
	for (i = 0; i < n; i++)
	{
    
    
		printf("%d, ", a[i]);
	}
	printf("\n");
}

int main()
{
    
    
	int a[] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int n = sizeof(a) / sizeof(a[0]);

	//数组名做函数参数
	printArrary(a, n); 
	return 0;
}

7.5.3 指针做为函数的返回值

#include <stdio.h>

int a = 10;

int *getA()
{
    
    
	return &a;
}


int main()
{
    
    
	*( getA() ) = 111;
	printf("a = %d\n", a);

	return 0;
}

7.6 指针和字符串

7.6.1 字符指针

#include <stdio.h>

int main()
{
    
    
	char str[] = "hello world";
	char *p = str;
	*p = 'm';
	p++;
	*p = 'i';
	printf("%s\n", str);

	p = "mike jiang";
	printf("%s\n", p);

	char *q = "test";
	printf("%s\n", q);

	return 0;
}

7.6.2 字符指针做函数参数

#include <stdio.h>

void mystrcat(char *dest, const char *src)
{
    
    
	int len1 = 0;
	int len2 = 0;
	while (dest[len1])
	{
    
    
		len1++;
	}
	while (src[len2])
	{
    
    
		len2++;
	}

	int i;
	for (i = 0; i < len2; i++)
	{
    
    
		dest[len1 + i] = src[i];
	}
}

int main()
{
    
    
	char dst[100] = "hello mike";
	char src[] = "123456";
	
	mystrcat(dst, src);
	printf("dst = %s\n", dst);

	return 0;
}

7.6.3 const修饰的指针变量

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    
    
	//const修饰一个变量为只读
	const int a = 10;
	//a = 100; //err

	//指针变量, 指针指向的内存, 2个不同概念
	char buf[] = "aklgjdlsgjlkds";

	//从左往右看,跳过类型,看修饰哪个字符
	//如果是*, 说明指针指向的内存不能改变
	//如果是指针变量,说明指针的指向不能改变,指针的值不能修改
	const char *p = buf;
	// 等价于上面 char const *p1 = buf;
	//p[1] = '2'; //err
	p = "agdlsjaglkdsajgl"; //ok

	char * const p2 = buf;
	p2[1] = '3';
	//p2 = "salkjgldsjaglk"; //err

	//p3为只读,指向不能变,指向的内存也不能变
	const char * const p3 = buf;

	return 0;
}

7.6.4 指针数组做为main函数的形参

int main(int argc, char *argv[]);
  • main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型
  • argv是命令行参数的字符串数组
  • argc代表命令行参数的数量,程序名字本身算一个参数
#include <stdio.h>

//argc: 传参数的个数(包含可执行程序)
//argv:指针数组,指向输入的参数
int main(int argc, char *argv[])
{
    
    

	//指针数组,它是数组,每个元素都是指针
	char *a[] = {
    
     "aaaaaaa", "bbbbbbbbbb", "ccccccc" };
	int i = 0;

	printf("argc = %d\n", argc);
	for (i = 0; i < argc; i++)
	{
    
    
		printf("%s\n", argv[i]);
	}
	return 0;
}

7.6.5 字符串处理函数

  1. strcpy()
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

注意:如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况。

	char dest[20] = "123456789";
	char src[] = "hello world";
	strcpy(dest, src);
	printf("%s\n", dest);
  1. strncpy()
#include <string.h>
char *strncpy(char *dest, const char *src, size_t n);
功能:把src指向字符串的前n个字符复制到dest所指向的空间中,是否拷贝结束符看指定的长度是否包含'\0'。
参数:
	dest:目的字符串首地址
	src:源字符首地址
	n:指定需要拷贝字符串个数
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

	char dest[20] ;
	char src[] = "hello world";

	strncpy(dest, src, 5);
	printf("%s\n", dest);

	dest[5] = '\0';
	printf("%s\n", dest);
  1. strcat()
#include <string.h>
char *strcat(char *dest, const char *src);
功能:将src字符串连接到dest的尾部,‘\0’也会追加过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
返回值:
	成功:返回dest字符串的首地址
	失败:NULL
	char str[20] = "123";
	char *src = "hello world";
	printf("%s\n", strcat(str, src));
  1. strncat()
#include <string.h>
char *strncat(char *dest, const char *src, size_t n);
功能:将src字符串前n个字符连接到dest的尾部,‘\0’也会追加过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
	n:指定需要追加字符串个数
返回值:
	成功:返回dest字符串的首地址
	失败:NULL
	char str[20] = "123";
	char *src = "hello world";
	printf("%s\n", strncat(str, src, 5));
  1. strcmp()
#include <string.h>
int strcmp(const char *s1, const char *s2);
功能:比较 s1 和 s2 的大小,比较的是字符ASCII码大小。
参数:
	s1:字符串1首地址
	s2:字符串2首地址
返回值:
	相等:0
	大于:>0 在不同操作系统strcmp结果会不同   返回ASCII差值
	小于:<0
	char *str1 = "hello world";
	char *str2 = "hello mike";

	if (strcmp(str1, str2) == 0)
	{
    
    
		printf("str1==str2\n");
	}
	else if (strcmp(str1, str2) > 0)
	{
    
    
		printf("str1>str2\n");
	}	
	else
	{
    
    
		printf("str1<str2\n");
	}
  1. strncmp()
#include <string.h>
int strncmp(const char *s1, const char *s2, size_t n);
功能:比较 s1 和 s2 前n个字符的大小,比较的是字符ASCII码大小。
参数:
	s1:字符串1首地址
	s2:字符串2首地址
	n:指定比较字符串的数量
返回值:
	相等:0
	大于: > 0
	小于: < 0
	char *str1 = "hello world";
	char *str2 = "hello mike";

	if (strncmp(str1, str2, 5) == 0)
	{
    
    
		printf("str1==str2\n");
	}
	else if (strcmp(str1, "hello world") > 0)
	{
    
    
		printf("str1>str2\n");
	}
	else
	{
    
    
		printf("str1<str2\n");
	}
  1. sprintf()
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符 '\0'  为止。
参数:
	str:字符串首地址
	format:字符串格式,用法和printf()一样
返回值:
	成功:实际格式化的字符个数
	失败: - 1
	char dst[100] = {
    
     0 };
	int a = 10;
	char src[] = "hello world";
	printf("a = %d, src = %s", a, src);
	printf("\n");

	int len = sprintf(dst, "a = %d, src = %s", a, src);
	printf("dst = \" %s\"\n", dst);
	printf("len = %d\n", len);
  1. sscanf()
#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:
	str:指定的字符串首地址
	format:字符串格式,用法和scanf()一样
返回值:
	成功:参数数目,成功转换的值的个数
	失败: - 1
	char src[] = "a=10, b=20";
	int a;
	int b;
	sscanf(src, "a=%d,  b=%d", &a, &b);
	printf("a:%d, b:%d\n", a, b);
  1. strchr()
#include <string.h>
char *strchr(const char *s, int c);
功能:在字符串s中查找字母c出现的位置
参数:
	s:字符串首地址
	c:匹配字母(字符)
返回值:
	成功:返回第一次出现的c地址
	失败:NULL
	char src[] = "ddda123abcd";
	char *p = strchr(src, 'a');
	printf("p = %s\n", p);
  1. strstr()
#include <string.h>
char *strstr(const char *haystack, const char *needle);
功能:在字符串haystack中查找字符串needle出现的位置
参数:
	haystack:源字符串首地址
	needle:匹配字符串首地址
返回值:
	成功:返回第一次出现的needle地址
	失败:NULL
	char src[] = "ddddabcd123abcd333abcd";
	char *p = strstr(src, "abcd");
	printf("p = %s\n", p);
  1. strtok()
#include <string.h>
char *strtok(char *str, const char *delim);
功能:来将字符串分割成一个个片段。当strtok()在参数s的字符串中发现参数delim中包含的分割字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0。
参数:
	str:指向欲分割的字符串
	delim:为分割字符串中包含的所有字符
返回值:
	成功:分割后字符串首地址
	失败:NULL
  • 在第一次调用时:strtok()必需给予参数s字符串
  • 往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针
	char a[100] = "adc*fvcv*ebcy*hghbdfg*casdert";
	char *s = strtok(a, "*");//将"*"分割的子串取出
	while (s != NULL)
	{
    
    
		printf("%s\n", s);
		s = strtok(NULL, "*");
	}
  1. atoi()
#include <stdlib.h>
int atoi(const char *nptr);
功能:atoi()会扫描nptr字符串,跳过前面的空格字符,直到遇到数字或正负号才开始做转换,而遇到非数字或字符串结束符('\0')才结束转换,并将结果返回返回值。
参数:
	nptr:待转换的字符串
返回值:成功转换后整数

类似的函数有:

  • atof():把一个小数形式的字符串转化为一个浮点数
  • atol():将一个字符串转化为long类型
	char str1[] = "-10";
	int num1 = atoi(str1);
	printf("num1 = %d\n", num1);

	char str2[] = "0.123";
	double num2 = atof(str2);
	printf("num2 = %lf\n", num2);

7.7 指针小结

在这里插入图片描述

8. 内存管理

8.1 作用域

C语言变量的作用域分为:

  • 代码块作用域(代码块是{}之间的一段代码)
  • 函数作用域
  • 文件作用域

8.1.1 局部变量

局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点:

  • 在一个函数内定义,只在函数范围内有效
  • 在复合语句中定义,只在复合语句中有效
  • 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束
  • 如果没有赋初值,内容为随机
#include <stdio.h>

void test()
{
    
    
	//auto写不写是一样的
	//auto只能出现在{}内部
	auto int b = 10; 
}

int main(void)
{
    
    
	//b = 100; //err, 在main作用域中没有b

	if (1)
	{
    
    
		//在复合语句中定义,只在复合语句中有效
		int a = 10;
		printf("a = %d\n", a);
	}

	//a = 10; //err离开if()的复合语句,a已经不存在
	
	return 0;
}

8.1.2 静态(static)局部变量

  • static局部变量的作用域也是在定义的函数内有效
  • static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
  • static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
#include <stdio.h>

void fun1()
{
    
    
	int i = 0;
	i++;
	printf("i = %d\n", i);
}

void fun2()
{
    
    
	//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
	static int a;
	a++;
	printf("a = %d\n", a);
}

int main(void)
{
    
    
	fun1();
	fun1();
	fun2();
	fun2();
	
	return 0;
}

8.1.3 全局变量

  • 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明
  • 全局变量的生命周期和程序运行周期一样
  • 不同文件的全局变量不可重名

8.1.4 静态(static)全局变量

  • 在函数外定义,作用范围被限制在所定义的文件中
  • 不同文件静态全局变量可以重名,但作用域不冲突
  • static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次

8.1.5 extern全局变量声明

extern int a;声明一个变量,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义。

8.1.6 全局函数和静态函数

在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。

对于不同文件中的staitc函数名字可以相同。
在这里插入图片描述

注意:

  • 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰
  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
  • 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的

8.1.7 总结

在这里插入图片描述

8.2 内存布局

8.2.1 内存分区

C代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。
在 Windows 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
在这里插入图片描述

通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

  • 代码区
    存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
  • 全局初始化数据区/静态数据区(data段)
    该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
  • 未初始化数据区(又叫 bss 区)
    存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
在这里插入图片描述

  • 代码区(text segment)
    加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
  • 未初始化数据区(BSS)
    加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
  • 全局初始化数据区/静态数据区(data segment)
    加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
  • 栈区(stack)
    栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
  • 堆区(heap)
    堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

8.2.2 存储类型总结

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

int e;
static int f;
int g = 10;
static int h = 10;
int main()
{
    
    
	int a;
	int b = 10;
	static int c;
	static int d = 10;
	char *i = "test";
	char *k = NULL;

	printf("&a\t %p\t //局部未初始化变量\n", &a);
	printf("&b\t %p\t //局部初始化变量\n", &b);

	printf("&c\t %p\t //静态局部未初始化变量\n", &c);
	printf("&d\t %p\t //静态局部初始化变量\n", &d);

	printf("&e\t %p\t //全局未初始化变量\n", &e);
	printf("&f\t %p\t //全局静态未初始化变量\n", &f);

	printf("&g\t %p\t //全局初始化变量\n", &g);
	printf("&h\t %p\t //全局静态初始化变量\n", &h);

	printf("i\t %p\t //只读数据(文字常量区)\n", i);

	k = (char *)malloc(10);
	printf("k\t %p\t //动态分配的内存\n", k);

	return 0;
}

8.2.3 内存操作函数

1) memset()

#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
	s:需要操作内存s的首地址
	c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
	n:指定需要设置的大小
返回值:s的首地址
	int a[10];

	memset(a, 0, sizeof(a));
	memset(a, 97, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%c\n", a[i]);
	}

2) memcpy()

#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
	dest:目的内存首地址
	src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
	n:需要拷贝的字节数
返回值:dest的首地址
	int a[10] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10];
	
	memcpy(b, a, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d, ", b[i]);
	}
	printf("\n");

	//memcpy(&a[3], a, 5 * sizeof(int)); //err, 内存重叠

3) memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。

4) memcmp()

#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
	s1:内存首地址1
	s2:内存首地址2
	n:需比较的前n个字节
返回值:
	相等:=0
	大于:>0
	小于:<0
	int a[10] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10] = {
    
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	int flag = memcmp(a, b, sizeof(a));
	printf("flag = %d\n", flag);

8.2.4 堆区内存分配和释放

1)malloc()

#include <stdlib.h>
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
	size:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL
#include <stdlib.h> 
#include <stdio.h>
#include <string.h>

int main()
{
    
    
	int count, *array, n;
	printf("请输入要申请数组的个数:\n");
	scanf("%d", &n);

	array = (int *)malloc(n * sizeof (int));
	if (array == NULL)
	{
    
    
		printf("申请空间失败!\n");
		return -1;
	}
	//将申请到空间清0
	memset(array, 0, sizeof(int)*n);

	for (count = 0; count < n; count++) /*给数组赋值*/
		array[count] = count;

	for (count = 0; count < n; count++) /*打印数组元素*/
		printf("%2d", array[count]);

	free(array);

	return 0;
}

2)free()

#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
参数:
ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
返回值:无

8.3 内存分区代码分析

1) 返回栈区地址

#include <stdio.h>
int *fun()
{
    
    
	int a = 10;
	return &a;//函数调用完毕,a释放
}

int main(int argc, char *argv[])
{
    
    
	int *p = NULL;
	p = fun();
	*p = 100; //操作野指针指向的内存,err

	return 0;
}

2) 返回data区地址

#include <stdio.h>

int *fun()
{
    
    
	static int a = 10;
	return &a; //函数调用完毕,a不释放
}

int main(int argc, char *argv[])
{
    
    
	int *p = NULL;
	p = fun();
	*p = 100; //ok
	printf("*p = %d\n", *p);

	return 0;
}

3) 值传递1

#include <stdio.h>
#include <stdlib.h>

void fun(int *tmp)
{
    
    
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
}

int main(int argc, char *argv[])
{
    
    
	int *p = NULL;
	fun(p); //值传递,形参修改不会影响实参
	printf("*p = %d\n", *p);//err,操作空指针指向的内存

	return 0;
}

4) 值传递2

#include <stdio.h>
#include <stdlib.h>

void fun(int *tmp)
{
    
    
	*tmp = 100;
}

int main(int argc, char *argv[])
{
    
    
	int *p = NULL;
	p = (int *)malloc(sizeof(int));

	fun(p); //值传递
	printf("*p = %d\n", *p); //ok,*p为100

	return 0;
}

5) 返回堆区地址

#include <stdio.h>
#include <stdlib.h>

int *fun()
{
    
    
	int *tmp = NULL;
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
	return tmp;//返回堆区地址,函数调用完毕,不释放
}

int main(int argc, char *argv[])
{
    
    
	int *p = NULL;
	p = fun();
	printf("*p = %d\n", *p);//ok

	//堆区空间,使用完毕,手动释放
	if (p != NULL)
	{
    
    
		free(p);
		p = NULL;
	}

	return 0;
}

9. 复合类型(自定义类型)

9.1 结构体

9.1.1概述

数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。

有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。

C语言中给出了另一种构造数据类型——结构体。
在这里插入图片描述

9.1.2 结构体变量的定义和初始化

定义结构体变量的方式:

  • 先声明结构体类型再定义变量名
  • 在声明类型的同时定义变量
  • 直接定义结构体类型变量(无类型名)

在这里插入图片描述

结构体类型和结构体变量关系:

  • 结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。
  • 结构体变量:系统根据结构体类型(内部成员状况)为之分配空间。
//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

//先定义类型,再定义变量(常用)
struct stu s1 = {
    
     "mike", 18 };


//定义类型同时定义变量
struct stu2
{
    
    
	char name[50];
	int age;
}s2 = {
    
     "lily", 22 };

struct
{
    
    
	char name[50];
	int age;
}s3 = {
    
     "yuri", 25 };

9.1.3 结构体成员的使用

#include<stdio.h>
#include<string.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

int main()
{
    
    
	struct stu s1;

	//如果是普通变量,通过点运算符操作结构体成员
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);

	//如果是指针变量,通过->操作结构体成员
	strcpy((&s1)->name, "test");
	(&s1)->age = 22;
	printf("(&s1)->name = %s, (&s1)->age = %d\n", (&s1)->name, (&s1)->age);

	return 0;
}

9.1.4 结构体数组

#include <stdio.h>

//统计学生成绩
struct stu
{
    
    
	int num;
	char name[20];
	char sex;
	float score;
};

int main()
{
    
    
	//定义一个含有5个元素的结构体数组并将其初始化
	struct stu boy[5] = {
    
    
		{
    
     101, "Li ping", 'M', 45 },			
		{
    
     102, "Zhang ping", 'M', 62.5 },
		{
    
     103, "He fang", 'F', 92.5 },
		{
    
     104, "Cheng ling", 'F', 87 },
		{
    
     105, "Wang ming", 'M', 58 }};

	int i = 0;
	int c = 0;
	float ave, s = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		s += boy[i].score;	//计算总分
		if (boy[i].score < 60)
		{
    
    
			c += 1;		//统计不及格人的分数
		}
	}

	printf("s=%f\n", s);//打印总分数
	ave = s / 5;					//计算平均分数
	printf("average=%f\ncount=%d\n\n", ave, c); //打印平均分与不及格人数


	for (i = 0; i < 5; i++)
	{
    
    
		printf(" name=%s,  score=%f\n", boy[i].name, boy[i].score);
           // printf(" name=%s,  score=%f\n", (boy+i)->name, (boy+i)->score);

	}

	return 0;
}

9.1.5 结构体套结构体

#include <stdio.h>

struct person
{
    
    
	char name[20];
	char sex;
};

struct stu
{
    
    
	int id;
	struct person info;
};

int main()
{
    
    
	struct stu s[2] = {
    
     1, "lily", 'F', 2, "yuri", 'M' };

	int i = 0;
	for (i = 0; i < 2; i++)
	{
    
    
		printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex);
	}

	return 0;
}

9.1.6 结构体赋值

#include<stdio.h>
#include<string.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

int main()
{
    
    
	struct stu s1;

	//如果是普通变量,通过点运算符操作结构体成员
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);

	//相同类型的两个结构体变量,可以相互赋值
	//把s1成员变量的值拷贝给s2成员变量的内存
	//s1和s2只是成员变量的值一样而已,它们还是没有关系的两个变量
	struct stu s2 = s1;
//memcpy(&s2, &s1, sizeof(s1));
	printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age);

	return 0;
}

9.1.7 结构体和指针

1)指向普通结构体变量的指针

#include<stdio.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

int main()
{
    
    
	struct stu s1 = {
    
     "lily", 18 };

	//如果是指针变量,通过->操作结构体成员
	struct stu *p = &s1;
	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n",  (*p).name,  (*p).age);

	return 0;
}

2)堆区结构体变量

#include<stdio.h>
#include <string.h>
#include <stdlib.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

int main()
{
    
    
	struct stu *p = NULL;

	p = (struct stu *)malloc(sizeof(struct  stu));

	//如果是指针变量,通过->操作结构体成员
	strcpy(p->name, "test");
	p->age = 22;

	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n", (*p).name,  (*p).age);

	free(p);
	p = NULL;

	return 0;
}

3)结构体套一级指针

#include<stdio.h>
#include <string.h>
#include <stdlib.h>

//结构体类型的定义
struct stu
{
    
    
	char *name; //一级指针
	int age;
};

int main()
{
    
    
	struct stu *p = NULL;

	p = (struct stu *)malloc(sizeof(struct  stu));

	p->name = malloc(strlen("test") + 1);
	strcpy(p->name, "test");
	p->age = 22;

	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);

	if (p->name != NULL)
	{
    
    
		free(p->name);
		p->name = NULL;
	}

	if (p != NULL)
	{
    
    
		free(p);
		p = NULL;
	}

	return 0;
}

9.1.8 结构体做函数参数

1)结构体普通变量做函数参数

#include<stdio.h>
#include <string.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

//函数参数为结构体普通变量
void set_stu(struct stu tmp)
{
    
    
	strcpy(tmp.name, "mike");
	tmp.age = 18;
	printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age);
}

int main()
{
    
    
	struct stu s = {
    
     0 };
	set_stu(s); //值传递
	printf("s.name = %s, s.age = %d\n", s.name, s.age);

	return 0;
}

2)结构体指针变量做函数参数

#include<stdio.h>
#include <string.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

//函数参数为结构体指针变量
void set_stu_pro(struct stu *tmp)
{
    
    
	strcpy(tmp->name, "mike");
	tmp->age = 18;
}

int main()
{
    
    
	struct stu s = {
    
     0 };
	set_stu_pro(&s); //地址传递
	printf("s.name = %s, s.age = %d\n", s.name, s.age);

	return 0;
}

3)结构体数组名做函数参数

#include<stdio.h>

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

//void set_stu_pro(struct stu tmp[100], int n)
//void set_stu_pro(struct stu tmp[], int n)
void set_stu_pro(struct stu *tmp, int n)
{
    
    
	int i = 0;
	for (i = 0; i < n; i++)
	{
    
    
		sprintf(tmp->name, "name%d%d%d", i, i, i);
		tmp->age = 20 + i;
		tmp++;
	}
}

int main()
{
    
    
	struct stu s[3] = {
    
     0 };
	int i = 0;
	int n = sizeof(s) / sizeof(s[0]);
	set_stu_pro(s, n); //数组名传递

	for (i = 0; i < n; i++)
	{
    
    
		printf("%s, %d\n", s[i].name, s[i].age);
	}

	return 0;
}

4)const修饰结构体指针形参变量

//结构体类型的定义
struct stu
{
    
    
	char name[50];
	int age;
};

void fun1(struct stu * const p)
{
    
    
	//p = NULL; //err
	p->age = 10; //ok
}

//void fun2(struct stu const*  p)
void fun2(const struct stu *  p)
{
    
    
	p = NULL; //ok
	//p->age = 10; //err
}

void fun3(const struct stu * const p)
{
    
    
	//p = NULL; //err
	//p->age = 10; //err
}

9.2 共用体(联合体)

  • 联合union是一个能在同一个存储空间存储不同类型数据的类型
  • 联合体所占的内存长度等于其最长成员的长度倍数,也有叫做共用体
  • 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
  • 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
  • 共用体变量的地址和它的各成员的地址都是同一地址
#include <stdio.h>

//共用体也叫联合体 
union Test
{
    
    
	unsigned char a;
	unsigned int b;
	unsigned short c;
};

int main()
{
    
    
	//定义共用体变量
	union Test tmp;

	//1、所有成员的首地址是一样的
	printf("%p, %p, %p\n", &(tmp.a), &(tmp.b), &(tmp.c));

	//2、共用体大小为最大成员类型的大小
	printf("%lu\n", sizeof(union Test));

	//3、一个成员赋值,会影响另外的成员
	//左边是高位,右边是低位
	//低位放低地址,高位放高地址
	tmp.b = 0x44332211;

	printf("%x\n", tmp.a); //11
	printf("%x\n", tmp.c); //2211

	tmp.a = 0x00;
	printf("short: %x\n", tmp.c); //2200
	printf("int: %x\n", tmp.b); //44332200

	return 0;
}

9.3 枚举

枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。

枚举类型定义:

enum  枚举名
{
    
    
	枚举值表
};
  • 在枚举值表中应列出所有可用值,也称为枚举元素。
  • 枚举值是常量,不能在程序中用赋值语句再对它赋值
  • 举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …
#include <stdio.h>

enum weekday
{
    
    
	sun = 2, mon, tue, wed, thu, fri, sat
} ;

enum bool
{
    
    
	flase, true
};

int main()
{
    
    
	enum weekday a, b, c;
	a = sun;
	b = mon;
	c = tue;
	printf("%d,%d,%d\n", a, b, c);

	enum bool flag;
	flag = true;

	if (flag == 1)
	{
    
    
		printf("flag为真\n");
	}
	return 0;
}

9.4 typedef

typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型

  • 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
  • #define发生在预处理,typedef发生在编译阶段
#include <stdio.h>

typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;

typedef struct type
{
    
    
	UBYTE a;
	INT b;
	T_BYTE c;
}TYPE, *PTYPE;

int main()
{
    
    
	TYPE t;
	t.a = 254;
	t.b = 10;
	t.c = 'c';

	PTYPE p = &t;
	printf("%u, %d, %c\n", p->a, p->b, p->c);

	return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_63260018/article/details/132844107