C语言基础

文章目录


参考链接菜鸟教程 C语言教程

1. 简介

C 语言是一种通用的、面向过程式的计算机程序设计语言。

C与C++

• C语言是结构化和模块化的语言,面向过程。
• C++保留了C语言原有的所有优点,增加了面向对象的机制,俗称“带类的C",1983年更名为C++

开发工具

• windows可使用记事本(Notepad++)+命令行,vim/vi可使用在linux操作系统上。
• VS 2015等:功能强大,体积也强大

C编译器安装

windows上安装
windonws上安装时需要先安装MinGW,然后再安装gcc、g++等。

Linux上安装
可参考Linux命令7.2节

C程序实例

C 程序主要包括以下部分:

  • 预处理器指令
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释

一个C程序就是由若干头文件和函数组成。

简单来说,一个C程序就是由若干头文件和函数组成。

#include <stdio.h>
 
int main()
{
    /* 我的第一个 C 程序 */
    printf("Hello, World! \n");
 
    return 0;
}

在这里插入图片描述

  • 程序第一行 #include <stdio.h>是一条预处理命令, 它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作。

  • 下一行 int main() 是主函数,程序从这里开始执行。所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。函数是实现代码逻辑的一个小的单元。

  • 下一行/* … */ 用于注释说明,会被编译器忽略。

  • printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。

  • return 0; 语句用于表示终止mian()函数,并返回值0;

  • stdio.h 是一个头文件(标准输入输出头文件), #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。

规范

  • 一个说明或一个语句占一行,例如:包含头文件、一个可执行语句结束都需要换行。

  • 函数体内的语句要有明显缩进,通常以按一下Tab键为一个缩进。

  • 括号要成对写,如果需要删除的话也要成对删除。

  • 当一句可执行语句结束的时候末尾需要有分号。

  • 代码中所有符号均为英文半角符号。

预处理命令

预处理程序(删除程序注释,执行预处理命令等)–>编译器编译源程序
• 宏定义:#define 标识符 字符串
• 文件包含:#include <filename>或者#include “filename”

引用区别搜索范围和先后顺序
<>从标准库中引用依次搜索系统目录,PATH环境变量所指目录
“”当我们自己创建的工程文件没有加入到标准库,用<>无法找到,所以使用“”一次搜索当前文件夹、系统目录,PATH环境目录
#和##的区别

#:把宏参数变成一个字符串;
##:把两个宏参数连接到一起(只能两个)

#define hehe(x,y) x##y
int main()
{
	char string[ ]="hello world!";
	printf("%s\n",hehe(str,ing));
   	system("pause"); 
    return 0; 
}

在这里插入图片描述
参考连接:#和##的区别

执行和编译C程序

Linux命令7.3节

2. C基本语法

C 程序由各种令牌组成,令牌可以是关键字、标识符、常量、字符串值,或者是一个符号。

注释

注释是写给程序员看的,不是写给自己看的。

//单行注释
/*
多行注释
多行注释
*/

关键字

关键字是C中的保留字,这些保留字不能为常量名、变量名和其他标识符名称。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

标识符

标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)。

C 标识符内不允许出现标点字符,比如 @、$ 和 %。

C 是区分大小写的编程语言。因此,在 C 中,Manpower 和 manpower 是两个不同的标识符。

标识符最好选择有意义的英文单词组成做到"见名知意",不要使用中文。

标识符不能是C语言的关键字。

C中的空格

只包含空格的行,被称为空白行,可能带有注释,C 编译器会完全忽略它。

在 C 中,空格用于描述空白符、制表符、换行符和注释。空格分隔语句的各个部分,让编译器能识别语句中的某个元素(比如 int)在哪里结束,下一个元素在哪里开始。因此,在下面的语句中:

int age;

在这里,int 和 age 之间必须至少有一个空格字符(通常是一个空白符),这样编译器才能够区分它们。

分号

在 C 程序中,分号是语句结束符。也就是说,每个语句必须以分号结束。它表明一个逻辑实体的结束。

return

  • 没有返回值的函数为空类型,用void表示,一旦函数的类型被定义为void,就不能再接收它的值了。为了使程序有良好的可读性并减少出错,凡不要求返回值的函数都应该定义为void类型
  • return语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数都只能有一个return语句执行,所以只有一个返回值。
  • 函数一旦遇到rerurn就立即返回,后面的语句都不会执行了,从这个角度看,return语句还有强制结束函数执行的作用。

return的用法:

  1. 为调用的函数返回参数值
    在具有返回值的函数中,返回一个函数值,这个返回参数可以是一个数、一个表达式。此应用最为普遍。
  2. 提前结束函数
    正如上面所述,函数一旦遇到rerurn就立即返回,后面的语句都不会执行了,所以可用来终止函数的调用。return后面可以跟参数,也可以不跟,代表结束此函数。
  3. 返回一个函数
    如果return后面跟着一个函数,代表跳出此调用函数,并且跳出后执行return后的函数,然后急继续在主函数中执行程序。

3. 数据类型

数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。

基本的数据类型:
在这里插入图片描述
注:一些基本类型可以使用一个或多个类型修饰符进行修饰,比如:signed short int简写为short、signed long int 简写为long。

类型的意义:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围).
  2. 如何看待内存空间的视角,如下图整型和float型的10存储方式不一样。

在这里插入图片描述

C语言类型

  1. 内置类型
  2. 自定义类型
    在这里插入图片描述
    数组类型和结构类型统称为聚合类型。函数的类型指的是函数返回值的类型。

整数类型

在这里插入图片描述
各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。

windows的32位和64位大小相同。
在这里插入图片描述
为了得到某个类型或某个变量在特定平台上的准确大小,可以使用 sizeof 运算符。表达式 sizeof(type) 得到对象或类型的存储字节大小。

#include <stdio.h>
#include <limits.h>
 
int main()
{
   printf("int 存储大小 : %lu \n", sizeof(int));
   
   return 0;
}
int存储大小:4

浮点类型

在这里插入图片描述
头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节,float (单精度)浮点型 double 双精度浮点型。

#include <stdio.h>
#include <float.h>
 
int main()
{
   printf("float 存储最大字节数 : %lu \n", sizeof(float));
   printf("float 最小值: %E\n", FLT_MIN );
   printf("float 最大值: %E\n", FLT_MAX );
   printf("精度值: %d\n", FLT_DIG );
   
   return 0;
}

在这里插入图片描述

构造类型

数组类型
结构体类型 struct
枚举类型 enum
联合类型 union

指针类型

int* pi;
char* pc;
float* pf;
void* pv; 无具体类型的指针
指针类型,是一种特殊的类型,特点:首先大小是统一的,都是4或8字节,用来存放地址。

void(空)类型

在这里插入图片描述

枚举类型

C中的一种基本数据类型,它是由用户定义的若干枚举常量的集合;枚举元素是一个整型,枚举型可以隐式的转换为int型,int型不能隐式的转换为枚举型。

//枚举类型的语法:
enum 枚举名{
	 标识符[=整型常数], 
     标识符[=整型常数], 
... 
    标识符[=整型常数]
}枚举变量

如果枚举没有初始化, 即省掉"=整型常数"时, 则从第一个标识符开始;
默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。

如一星期7天:
#define定义

#define MON  1
#define TUE  2
#define WED  3
#define THU  4
#define FRI  5
#define SAT  6
#define SUN  7

枚举定义:

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
}; //只声明了枚举类型

枚举的优点为:什么使用枚举?
我们可以使用#define定义常量,为什么非要使用枚举?枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

C语言的源代码—>预编译---->编译—>链接---->可执行程序
预编译阶段会把注释、宏定义等进行替换处理,#define宏定义的是整型,enum是枚举类型,类型不一样。

枚举常量的定义
1… 先定义枚举类型,再定义枚举变量

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
  1. 定义枚举类型的同时定义枚举变量
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
  1. 省略枚举名称,直接定义枚举变量
enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

实例

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
 
int main()
{
    enum DAY day;
    day = WED;
    printf("%d",day);
    return 0;
}

枚举类型的遍历

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) {
        printf("枚举元素:%d \n", day);
    }
}

在这里插入图片描述

整型在内存中的存储

一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?

比如:
int a = 20;
int b =-10;
我们知道为a分配四个字节的空间。那如何存储?

原码、反码、补码

计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示万法均有符号位和数值位两部分,符号位都是用0表示"正",用1表示"负”,而数值位三种表示方法各不相同。

原码: 直接格二进制按照正负数的形式翻译成二进制就可以。
反码: 将原码的符号位不变,其他位依次按位取反就可以得到了。
补码: 反码+1得到补码。
int main(){
int a = 20;//4个字节-32bit
//00000000000000000000000000010100 -原码
//00000000000000000000000000010100 -反码
//00000000000000000000000000010100 -补码
//0x00000014
int b = -10;
//10000000000000000000000000001010 -原码
//11111111111111111111111111110101 -反码
//11111111111111111111111111110110 -补码
//0xFFFFFFF6
return 0;
)

在这里插入图片描述
在这里插入图片描述
小段存储,前面是低地址处,后面是高地址处。

对于整型:数据存放内存中其实存放的是补码
为什么呢?
在计算机系统中,数值一律用补码来表示和存储原因在于,使用补码,可以将符号位和数值域统一处理,同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

int main()
{
	1-1;
	// 1+(-1)
	// 00000000 00000000 00000000 00000001
	// 10000000 00000000 00000000 00000001
	// 10000000 00000000 00000000 00000010
	// 原码相加结果为-2
	// 00000000 00000000 00000000 00000001
	// 11111111 11111111 11111111 11111111
	// 00000000 00000000 00000000 00000000
	// 补码相加结果为0
	return 0;
}

在这里插入图片描述

大小端介绍

什么大端小端:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中.而数据的高位.,保存在内存的高地址中。

为什么有大端和小端

在这里插入图片描述
11223344大端存储 44332211小端存储

写一段代码告诉我们当前机器的字节序是什么?
给a赋值为1,从中取出第一个字节判断是0还是1,如果是0则为大端存储,如果是1则为小端存储。那么如何拿出第一个字节呢,我们想到指针的大小是根据类型不同的,

int main()
{
	int a=1;
	//int *p = &a;//正常指针类型为int
	char *p =(char*) &a;//从int*到char*类型不兼容,使用强制类型转换
	if(*p == 1)
	{
		printf("小端\n");
    }
    else
    {
    	printf("大端\n");
    }
	return 0;
}
#inc1ude <stdio.h>
int main()
{
	char a = -1;//默认有符号
	//10000000 00000000 00000000 00000001
	//11111111 11111111 11111111 11111110
	//11111111 11111111 11111111 11111111
	//11111111
	//11111111 11111111 11111111 11111111 整型提升 
	signed char b =-1;
	//11111111
	unsigned char c = -1;
	//11111111
	//00000000 00000000 00000000 11111111 无符号数整型提升前面补0,且原码补码相同
	printf("a=%d , b=%d , c=%d", a, b,c);//以d打印,十进制无符号整型,需要进行整型提升。
	return 0;
}

-1 -1 255

#include <stdio.h>int main()
{
	char a = -128;
	//10000000 00000000 00000000 10000000
	//11111111 11111111 11111111 01111111
	//11111111 11111111 11111111 10000000-补码
	//10000000
	//11111111 11111111 11111111 10000000-整型提升后的结果,无符号打印,原码补码相同
	printf("%u\n",a);
	//%d-打印十进制的有符号数字
	//%u-打印十进制的无符号数字
	return 0;
}

4294967168

#include <stdio.h>int main()
{
	char a = 128;
	//128=127+1,128和-128一样
	//128特殊处理

	printf("%u\n",a);
	//%d-打印十进制的有符号数字
	//%u-打印十进制的无符号数字
	return 0;
}

4294967168
在这里插入图片描述
在这里插入图片描述

int main()
{
	int i = -20;
	//10000000 00000000 00000000 00010100
	//11111111 11111111 11111111 11101011
	//11111111 11111111 11111111 11101100-补码
	unsigned int = 10;
	//00000000 00000000 00000000 00001010-补码
	//11111111 11111111 11111111 11110110-相加的补码
	//11111111 11111111 11111111 11110101
	//10000000 00000000 00000000 00001010  -10
	printf("%d\n",i+j)
}

-10

#include <windows.h>
int main()
{
	unsigned int i;//无符号整型,永远大于等于0,死循环
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n",i);
		Sleep(100);
	}
	return 0;
}

在这里插入图片描述

int main()
{
	char a[1000];
	int i;
	for (i = 0; i<1000; i++)
	{
		a[i] = -1 - i;
		// -1 .... -128 -129 .. . -256
		// -1 .... -128 127 126 ....0 找到\0停止,不包括\0
	}
	printf("%d", strlen(a));
	return 0;

很经典的一题,char的范围是-128-127,不包括0,答案是255。

#include <stdio.h>
unsigned char i = 0;//0-255,小于等于255条件恒成立
int main()
{
	for (i = 0; i <=255; i++)
	{
		printf("hello world\n");
	}
	return 0;
}

总结上面两个代码,使用无符号数和有符号数做判断条件时,要注意范围,否则容易造成死循环。

浮点型在内存中的存储

常见的浮点数:3.14159 1E10浮点数家族包括:float、double、long double类型。浮点数表示的范围:float.h中定义。
char、short、int、long在limit.h中,可以查看每个类型的字节数和最大最小值。

int main()
{
	double d = 1E10;
	printf("%lf",d);
	return 0;
}

在这里插入图片描述

int main
{
	int n = 9;
	//9.0
	//1001.0
	//(-1)^0*1.001*2^3
	//(-1)^s* M*  2^E
	//s---0
	//M---1.001  实际保存时只保存 0.001
	//E---3
    //0.5
    //0.1 二进制1*2^(-1)
    //1.0*2^(-1)=(-1)^0*2^(-1)
    //E--- -1
    //加上中间值存储,E+127=126
	float *pFloat = (float *)&n;
	//char* p = (char*)&n //&n原本类型是int*,要进行强制类型转换
	printf("n的值为:%d\n",n);
	printf("*pF1oat的值为:%f\n", *pFloat) ;
	*pFloat = 9.0;
	printf("num的值为:%d\n",n);
	printf("pFloat的值为:%f\n"",*pFloat);
	return 0;
}

在这里插入图片描述

整型放进去以整型拿出来是9,float拿出来变为0,float放进去以float拿出来是9.0,整型拿出来是109,说明整型和float存储方式不一致。

根据围际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:

  • (-1)SM2^E
  • (-1)^s表示符号位,当s-=0,V为正数;当s=1,V为负数,
  • M表示有效数字,大于等于1,小于2。
  • 2^E表示指数位。

举例来说:十进制的5.0,写成二进制是101.0,相当于1.01x2^2。那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。

十进制的-5.0,写成二进制是-101.0,相当于-1.01x22。那么,s=1,M=1.01,E=2。

IEEE 754规定:对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
在这里插入图片描述
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
在这里插入图片描述
IEEE 754对有效数字M和指数E,还有一些特别规定。前面说过,1<=M<=2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int) ,这意味着,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0-2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

int main()
{
	float f=5.5;
	//5.5
	//101.1
	//(-1)^0 * 1.011 * 2 ^ 2
	//M=1.011
	//E = 2
	//存储为2+127=129
	//0|1000000 1|0110000 00000000 00000000
	//0x40b00000
	return 0;
}

然后,指数E从内存中取出还可以再分成三种情况:

  1. E不全为0或不全为1(常规情况)
    这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023.),得到真实值,再将有效数字M前加上第一位的1。比如:0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示为0111110,而尾数1.0去掉整数部分为0,补齐0到23位,则其二进制表示形式为:
    0 01111110 00000000000000000000000
  2. E全为0
    这时,浮点数的指数E等于1-127(或1-1023]即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
    0 00000000 01100000000000000000000
    还原回去为0.112(-126),无限接近于0。
  3. E全为1
    这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
    0 11111111 00000000000000000000000
    255-127=128 表示无穷大
int main()
{
    int n = 9;
    //00000000 00000000 00000000 00001001 -补码
    //0 00000000 0000000 00000000 00001001 -浮点型,E全是0,表示一个无限接近于0的数字
	float *pFloat = (float *)&n;
	//char* p = (char*)&n //&n原本类型是int*,要进行强制类型转换
	printf("n的值为:%d\n",n);
	printf("*pF1oat的值为:%f\n", *pFloat) ;
	*pFloat = 9.0;
	//(-1)^0*1.001*2^3
	//0 10000010 1001 00000000 00000000 000
	//01000001 01001000 00000000 00000000
	//1091567616  
	printf("num的值为:%d\n",n);
	printf("pFloat的值为:%f\n"",*pFloat);
	return 0;
}

4. 变量

变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。

变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。

变量的定义

变量就是可以变化的量,而每个变量都会有一个名字(标识符)。变量占据内存中一定的存储单元。使用变量之前必须先定义变量,要区分变量名和变量值是两个不同的概念。

变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。

• 多个变量赋同一个值时,需要分别赋值

在这里插入图片描述

int x = y = z = 66;//错误
int x = 3,y = 3,z = 3;
int x, y ,z = 3;
x = y = z;
int a,b,c;
a=1;
b=2;
c=3;

int x, y ,z = 3; 变量可以在定义的时候被初始化(指定一个初始值)。初始化器由一个等号,后跟一个常量表达式组成。

不带初始化的定义:带有静态存储持续时间的变量会被隐式初始化为 NULL(所有字节的值都是 0),其他所有变量的初始值是未定义的。

变量声明

• 变量声明向编译器保证变量以给定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。
• 可以在 C程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。

变量的声明有两种情况:

1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。
除非有extern关键字,否则都是变量的定义。

extern int i; //声明,不是定义
int i; //声明,也是定义

声明与定义的区别

变量的声明(不分配内存):extern 数据类型 变量名;
变量的定义:数据类型 变量名1,变量名2,…变量名n;

// 变量声明
extern int a, b;
int main ()
{
  // 变量定义
  int a, b;
  // 初始化
  a = 23;
  b = 25;
  return 0;
}

实例

变量在头部就已经被声明,但是定义与初始化在主函数内

#include <stdio.h>
 
// 函数外定义变量 x 和 y
int x;
int y;
int addtwonum()
{
    // 函数内声明变量 x 和 y 为外部变量
    extern int x;
    extern int y;
    // 给外部变量(全局变量)x 和 y 赋值
    x = 1;
    y = 2;
    return x+y;
}
 
int main()
{
    int result;
    // 调用函数 addtwonum
    result = addtwonum();
    
    printf("result 为: %d",result);
    return 0;
}
result 为: 3

如果需要在一个源文件中引用另外一个源文件中定义的变量,我们只需在引用的文件中将变量加上 extern 关键字的声明即可。

在这里插入图片描述
在这里插入图片描述

5. 常量

常量是固定值,在程序执行期间不会改变。这些固定的值

常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。

常量就像是常规的变量,只不过常量的值在定义后不能进行修改。

整数常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。
在这里插入图片描述
在这里插入图片描述

浮点常量

浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。

当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

在这里插入图片描述

字符常量

字符常量是括在单引号中,例如,‘x’ 可以存储在 char 类型的简单变量中。

字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。

在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n)或制表符(\t)等。

在这里插入图片描述

字符串常量

字符串字面值或常量是括在双引号 “” 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

可以使用空格做分隔符,把一个很长的字符串常量进行分行。

下面的三种方式显示的字符串是相同的。
在这里插入图片描述

定义常量

在 C 中,有两种简单的定义常量的方式:

  1. 使用 #define 预处理器。
  2. 使用 const 关键字。
#define预处理器

#define identifier value

#include <stdio.h>
 
#define LENGTH 10   
#define WIDTH  5
#define NEWLINE '\n'
 
int main()
{
 
   int area;  
  
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);
 
   return 0;
}

在这里插入图片描述

const

const type variable = value;
const 声明常量要在一个语句内完成,常量在定义的时候必须同时被初始化;只读。
const int var = 5;

#include <stdio.h>
 
int main()
{
   const int  LENGTH = 10;
   const int  WIDTH  = 5;
   const char NEWLINE = '\n';
   int area;  
   
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);
 
   return 0;
}

通常将常量定义为大写字母形式。

定义指针

const int *p1;
int const *p2;
int* const p3;

第一二种情况,指针所指向的数据是只读的,p1,p2的值可以修改,但指向的数据不能被修改。三种情况中,第三种指针是只读的,p3本身的值不能被修改;

6. 运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C语言的运算符包括:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、杂项运算符。

算术运算符

在这里插入图片描述
a++和++a的区别:

#include <stdio.h>
 
int main()
{
   int c;
   int a = 10;
   c = a++; 
   printf("先赋值后运算:\n");
   printf("Line 1 - c 的值是 %d\n", c );
   printf("Line 2 - a 的值是 %d\n", a );
   a = 10;
   c = a--; 
   printf("Line 3 - c 的值是 %d\n", c );
   printf("Line 4 - a 的值是 %d\n", a );
 
   printf("先运算后赋值:\n");
   a = 10;
   c = ++a; 
   printf("Line 5 - c 的值是 %d\n", c );
   printf("Line 6 - a 的值是 %d\n", a );
   a = 10;
   c = --a; 
   printf("Line 7 - c 的值是 %d\n", c );
   printf("Line 8 - a 的值是 %d\n", a );
 
}

在这里插入图片描述

关系运算符

在这里插入图片描述

逻辑运算符

在这里插入图片描述

#include <stdio.h>
 
int main()
{
   int a = 5;
   int b = 20;
   int c ;
 
   if ( a && b )
   {
      printf("Line 1 - 条件为真\n" );
   }
   if ( a || b )
   {
      printf("Line 2 - 条件为真\n" );
   }
   /* 改变 a 和 b 的值 */
   a = 0;
   b = 10;
   if ( a && b )
   {
      printf("Line 3 - 条件为真\n" );
   }
   else
   {
      printf("Line 3 - 条件为假\n" );
   }
   if ( !(a && b) )
   {
      printf("Line 4 - 条件为真\n" );
   }
}

在这里插入图片描述

#include<stdio.h>

int main()
{  
    int i=0,a=0,b=2,c=3,d=4;
    i = a++ && ++b && d++;
    printf("a = %d\n  b = %d\n  c = %d\n  d = %d\n i = %d\n",a,b,c,d,i );

    return 0;
}

逻辑与,左边一个为假,右边就不再继续计算了。
在这里插入图片描述

#include<stdio.h>

int main()
{  
    int i=0,a=1,b=2,c=3,d=4;
    i = a++ && ++b && d++;
    printf("  a = %d\n  b = %d\n  c = %d\n  d = %d\n  i = %d\n",a,b,c,d,i);

    return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{  
    int i=0,a=1,b=2,c=3,d=4;
    i = a++ || ++b || d++;
    printf("a = %d\n  b = %d\n  c = %d\n  d = %d\n",a,b,c,d );

    return 0;
}

逻辑或,右边一个为真,右边就不再继续计算了。
在这里插入图片描述

#include<stdio.h>

int main()
{  
    int i=0,a=0,b=2,c=3,d=4;
    i = a++ || ++b || d++;
    printf("a = %d\n  b = %d\n  c = %d\n  d = %d\n",a,b,c,d );

    return 0;
}

在这里插入图片描述

位运算符

位运算符作用于位,并逐位执行操作。
假设A=60,B=13
二进制表示A=0011 1100,B=0000 1101。
在这里插入图片描述

#include<stdio.h>

int main()
{  
    int a = -1;
    //整数的二进制表示有:原码、反码、补码
    //存储到内存的是补码
    //10000000 00000000 00000000 00000001 -原码
    //11111111 11111111 11111111 11111110 -反码
    //11111111 11111111 11111111 11111111-补码

    int b = a>>1;
    
    printf("  a = %d\n  b = %d\n  c = %d\n",a,b,c);
    return 0;
}

在这里插入图片描述

左操作数:左边丢弃,右边补0;
右操作数:
1.算数右移,右边丢弃,左边补原符号位;
2. 逻辑右移,右边丢弃,左边补0。

例:交换两个数的值,不使用第三个变量。

int main()
{
	int a = 3;
	int b = 5;
	//加减法
	a = a+b;//相加可能会溢出,会丢失
	b = a-b;
	a = a-b;
	// 异或法
	a = a^b;
	b = a^b;
	a = a^b;//类似密码,异或结果和其中一个异或会得到另一个

}

例:求一个整数存储在内存中的二进制中的1的个数。

#include<stdio.h>

int main()
{  
    int num = 10;
    int count = 0;
    //统计num的补码中有几个1
    while (num)
    {
        if(num%2 == 1)
            count++;
        num = num/2;        
    }
    printf("二进制中1的个数 = %d\n",count);
    return 0;
}

在这里插入图片描述
如果num = -1,商0余-1,循环不再继续;此代码在负数时不适用。

可以用按位与的方法,将数字的每一位与1进行按位与计算,结果是1,该位则为1,int是4字节32bit;通过循环得到每一位是否为1。负数时首位补1,但是只计算32位,不影响结果。

#include<stdio.h>

int main()
{  
    int num = 10;
    int count = 0;
    int i = 0;
    for(i = 0;i<32;i++)
    {
        if(1 == ((num>>i)&1))
            count++;
    }
    printf("二进制中1的个数 = %d\n",count);
    return 0;
}

在这里插入图片描述
把一个数字的二进制的第三位置为1。

#include<stdio.h>

int main()
{  
    int a = 11;
    a = a|(1<<2);
    printf("%d\n",a);
    //00000000 00000000 00000000 00001011
    //00000000 00000000 00000000 00000100
    return 0;
}

在这里插入图片描述
将上述数字还原。

#include<stdio.h>

int main()
{  
    int a = 15;
    a = a&(~(1<<2));
    printf("%d\n",a);
    //00000000 00000000 00000000 00001111
    //11111111 11111111 11111111 11111011 与上面 与 即可得到
    //00000000 00000000 00000000 00000100  是上面数字的取反
    //00000000 00000000 00000000 00001011
    return 0;
}

在这里插入图片描述
++a和a++

#include<stdio.h>

int main()
{  
    int a = 10;

    printf("%d\n",a++); //后置++,先使用,后++
    printf("%d\n",++a); //前置++,先++,后使用

    return 0;
}

强制类型转换

#include<stdio.h>

int main()
{  
    int a = (int)3.14;
    return 0;
}

在这里插入图片描述

赋值运算符

多个运算符号的叫复合赋值符。
在这里插入图片描述

杂项运算符

sizeof

sizeof是C/C++中的关键字,它是一个运算符,其作用是取得一个对象(数据类型或者数据对象)的长度(即占用内存空间的大小,以byte为单位)。

sizeof(int) 将返回4。

• sizeof是运算符,不是函数。
• sizeof不能求得void类型的长度;
• sizeof能求得void类型的指针的长度;
• sizeof能求得静态分配内存的数组的长度!
• sizeof不能求得动态分配的内存的大小!
• sizeof不能对不完整的数组求长度;
• 当表达式作为sizeof的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值!
• sizeof可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体!
• sizeof求得的结构体(及其对象)的大小并不等于各个数据成员对象的大小之和!
• sizeof不能用于求结构体的位域成员的大小,但是可以求得包含位域成员的结构体的大小!

#include<stdio.h>

int main()
{  
    int a = 10;
    char c='r';
    char *p=&c;
    int arr[10] = {0};
    printf("%d\n",sizeof(a));//可表示为 sizeof a
    printf("%d\n",sizeof(int));
    
    printf("%d\n",sizeof(c));
    printf("%d\n",sizeof(char));
    
    printf("%d\n",sizeof(p));//64位平台,指针大小为8
    printf("%d\n",sizeof(char*));
    
    printf("%d\n",sizeof(arr));
    printf("%d\n",sizeof(int [10]));//数组也是有类型的
    return 0;
}

在这里插入图片描述

#include<stdio.h>

void test1(int arr[])//传入的是数组首元素的地址,接收的应该相当于指针,相当于int*
{
    printf("%d\n",sizeof(arr));
}
void test2(char ch[])//传入的是指针,相当于char*
{
    printf("%d\n",sizeof(ch));
}
int main()
{  
    int arr[10] = {0};
    char ch[10] = {0};
    printf("%d\n",sizeof(arr));
    printf("%d\n",sizeof(ch));
    
    test1(arr);
    test2(ch);
    return 0;
}

在这里插入图片描述

&

&a,返回变量的实际地址

* 指向一个变量

*a,将指向一个变量。

#include<stdio.h>

int main()
{  
    int a = 10;
    int *p = &a;//取地址操作符
    *p = 20;//解引用操作符
    return 0;
}
//p指向a的地址,*p解引用,*p就是a,所以当*p=20时,a的值变为20。
? 三元运算符

exp1?exp2:exp3
如果条件exp1为真 ? 则计算 Exp2 的值,结果即为整个 ? 表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值

#include <stdio.h>
 
int main()
{
   int a = 4;
   short b;
   double c;
   int* ptr;
 
   /* sizeof 运算符实例 */
   printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a) );
   printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b) );
   printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c) );
 
   /* & 和 * 运算符实例 */
   ptr = &a;    /* 'ptr' 现在包含 'a' 的地址 */
   printf("a 的值是 %d\n", a);
   printf("*ptr 是 %d\n", *ptr);
 
   /* 三元运算符实例 */
   a = 10;
   b = (a == 1) ? 20: 30;
   printf( "b 的值是 %d\n", b );
 
   b = (a == 10) ? 20: 30;
   printf( "b 的值是 %d\n", b );
}

在这里插入图片描述

逗号表达式

exp1,exp2,exp3,…,expN
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
在这里插入图片描述
c=13

在这里插入图片描述

下标引用、函数调用和结构成员
  1. []下标引用操作符
    操作数:一个数组名+一个索引值
int arr[10J;// 创建教组
arr[9] =10;//实用下标引用辣作符。
//[]的两个操作数是arr和9。
  1. ()函数调用操作符接受一个或者多个操作数;第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include <stdio.h>
int get_max(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	int a = 10;
	int b = 20;//调用函数的时候的()就是函数调用操作符
	int max = get_max(a, b);
	printf( "max =%d\n", max);
	return 0;
}
  1. 访问一个结构的成员

. 结构体.成员名
-> 结构体指针->成员名

#include<stdio.h>
//创建—个结构体类型-struct Stu
struct Stu
{
    char name[20];
    int age;
    char id[20];
};
int main()
{
    int a = 10;
    //使用struct Stu这个类型创建了一个学生对象s1,并初始化
    struct Stu s1 ={"张三",20,"2019010305"};
    struct Stu *ps = &s1;
    printf("%s \n",s1.name);
    printf("%d \n",s1.age);
    printf("%s \n",s1.id);
    printf("%s \n",(*ps).name);
    printf("%d \n",(*ps).age);
    printf("%s \n",(*ps).id);
    printf("%s \n",ps->name);
    printf("%d \n",ps->age);
    printf("%s \n",ps->id);
    return 0;
}

在这里插入图片描述

隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的学节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU ( general-purpose CPU )是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned in1,然后才能送入CPU去执行运算。
在这里插入图片描述

int main()
{
	char c = 1;
	printf("%Xu\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(lc));
	return 0;
}

c只要参与表达式运算,就会发生整形提升,表达式+c,就会发生提升,所以sizeof(+c)是4个字节。

算数转换

表达式2
c + --c
注释:同上,操作符的优先级只能决定自减-的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

表达式3-非法表达式
在这里插入图片描述

7. 语法结构

判断

判断结构指当有一个或多个要评估或测试的条件时,指定条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。

判断语句有两种形式:

  • if
    在这里插入图片描述
  • switch
    在这里插入图片描述
    • switch 语句中的 expression 是一个常量表达式,必须是一个整型或枚举类型。
    • 每个case后满的常量表达式必须各不相同。
    • case语句和default语句出现的顺序对执行结果没有影响。
    • 若case后没有break,执行完就不会判断,继续执行下一个case语句,直到遇到break。
    • default后面如果没有case,则break可以省略
    • 多个case可以用一组执行语句
    在这里插入图片描述

注意:三元运算符也是条件语句

循环

有的时候,我们可能需要多次执行同一块代码。循环语句允许我们多次执行一个语句或语句组。

  • while
    当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。
while(condition) //0为false,非0为true
{
	statement(s)
}
  • do……while
do
{
   statement(s);

}while( condition );

条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。

如果条件为真,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。这个过程会不断重复,直到给定条件变为假为止。

  • for
for(int; condition; increment) //0为false,非0或什么也不写为true
{
	statement(s)
}

1.init首先被执行,且只会执行一次,也可以不写任何语句。
2.然后会判断conditon,true执行循环主体,false跳过循环
3.执行完循环主体,执行increment,跳到2

8. 字符串

在 C 语言中,字符串实际上是使用空字符\0结尾的一维字符数组。因此,\0 是用于标记字符串的结束。

空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。

声明和初始化创建一个 RUNOOB 字符串。由于在数组的末尾存储了空字符 \0,所以字符数组的大小比单词 RUNOOB 的字符数多一个。

char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};

根据数组初始化规则,可写为

char site[] = "RUNOOB";
#include <stdio.h>
 
int main ()
{
   char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
 
   printf("菜鸟教程: %s\n", site );
 
   return 0;
}

在这里插入图片描述

9. 函数

函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。

函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

函数的声明与定义

声明函数

函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
return_type function_name( parameter list );

在这里插入图片描述

  • 函数声明中,参数的名称并不重要,只有参数的类型是必需的。
  • 在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。
函数定义

在这里插入图片描述

  • 函数类型:函数的返回值类型;有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
  • 函数名:必须符合C标识符命名规则,后面必须跟一对括号;
  • 函数体:实现函数功能的主体部分;
  • 参数列表:函数名后面的括号内,用于向函数传递数值或带回数值。
函数的定义与声明
  • 函数声明中,参数名可以省略,参数类型和函数的类型不能省略。
  • 只有分配存储空间的变量声明才叫变量定义,函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也要占存储空间。
  • 函数声明可以放在主调函数内部,放在调用语句之前;也可以放在主调函数外,如果位于所有定义函数之前,后面函数定义顺序任意,各个主调函数调用也不必再做声明
  • 当函数定义在前,函数调用之后,可以不用函数声明。
  • 后两条总结一下就是:调用函数前,程序得知道有这个函数,声明就是提前让程序知道有这么个函数
  • 没有函数体的函数声明有什么用呢?它为编译器提供了有用的信息,编译器在翻译代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,这样碰到函数调用时才知道怎么生成相应的指令,所以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则。

调用函数

当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。

函数参数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。

形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

当调用函数时,有两种向函数传递参数的方式:
在这里插入图片描述

作用域

作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。

C语言有三个地方可以声明变量:

  • 在函数或块内部的局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。
  • 在所有函数外部的全局变量。全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。
  • 在形式参数的函数参数定义中。函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。

在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。

初始化局部变量和全局变量
当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动对其初始化,默认值如下:
在这里插入图片描述

10. 数组

数组是一些具有相同数据类型或相同属性(类)的数据的集合,用数据名标识,用下标或序号区分各个数据。数组中的数据称为元素。

C语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

在这里插入图片描述
数组中的特定元素可以通过索引访问,第一个索引值为 0。
在这里插入图片描述

一维数组

定义一维数组的形式:数据类型 数据名[常量表达式]
初始化的形式:数据类型 数组名[常量表达式] = {初值表};
为数组的某一个元素赋值:数组名[下标] =值(下标从0开始)
数组的引用:组名[下标]
在这里插入图片描述

• 初始化数组时,可以只给部分数组元素赋值,叫不完全初始化,剩下的元素默认初始化为0。
• 对全部元素数组赋值时,可以不指定数组长度,编译系统会根据初值个数确定数组的长度。
• static型数组元素不赋初值,系统会自动默认为0。
• 创建数组时[]中必须为常量。
• sizeof(数组)大小为字符数加1;计算的是所占空间大小,可计算变量、数组、类型的大小。
• strlen(数组)大小为字符数,计算\0之前的字符数,只能针对字符串求长度。
• 计算数组长度时,字符型数组用strlen,数值型数组用sizeof。
• 数组在内存中是连续存放的。

#include <stdio.jih>
 
int main ()
{
   int n[10]; /* n 是一个包含 10 个整数的数组 */
   int i,j;
 
   /* 初始化数组元素 */         
   for ( i = 0; i < 10; i++ )
   {
      n[i] = i + 100; /* 设置元素 i 为 i + 100 */
   }
   
   /* 输出数组中每个元素的值 */
   for (j = 0; j < 10; j++ )
   {
      printf("Element[%d] = %d\n", j, n[j] );
   }
 
   return 0;
}

在这里插入图片描述
在这里插入图片描述
4 3 3 随机数
strlen只针对字符串,arr2不是字符串,遇到‘\0’的位置随机。

二维数组

定义二维数组的形式:数据类型 数组名[常量表达式1][常量表达式2]
初始化的形式:数据类型 数组名[常量表达式1] [常量表达式2]= {初值表};
为数组的某一个元素赋值:数组名[行下标][列下标] =值(下标从0开始)
数组的引用:数组名[行下标][列下标]

#include <stdio.h>
 
int main ()
{
   /* 一个带有 5 行 2 列的数组 */
   int a[5][2] = { {0,0}, {1,2}, {2,4}, {3,6},{4,8}};
   int i, j;
 
   /* 输出数组中每个元素的值 */
   for ( i = 0; i < 5; i++ )
   {
      for ( j = 0; j < 2; j++ )
      {
         printf("a[%d][%d] = %d\n", i,j, a[i][j] );
      }
   }
   return 0;
}

数组和函数

传递数组给函数
如果要将数组传递给函数,有三种方式可以声明函数的形式参数,三种方式最终都是告诉编译器将要接收一个整型指针。
数组传参,传过去的数组的首地址。

  1. 形式参数是一个指针:void function(int *param)
  2. 形式参数是一个已定义大小的数组:void function(int param[10])
  3. 形式参数是一个未定义大小的数组:void function(int param[])

二维数组:void function(int a[][3],int size)
如果传递二维数组,形参必须制定第二维的长度。

#include <stdio.h>
 
/* 函数声明 */
double getAverage(int arr[], int size); //对于调用的函数要先声明
 
int main ()
{
   /* 带有 5 个元素的整型数组 */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;  //主函数中返回值要先定义
 
   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;
 
   /* 输出返回值 */
   printf( "平均值是: %f ", avg );
    
   return 0;
}
 
double getAverage(int arr[], int size)
{
  int    i;
  double avg;
  double sum=0;
 
  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }
 
  avg = sum / size;
 
  return avg;
}

冒泡排序,将一个整型数组排序。

#include <stdio.h>
bubble_sort(int arr[],int sz)
{
 // 确定冒泡排序的趟数
 int i = 0;
 //int sz = sizeof(arr)/sizeof(arr[0]);arr传过去的是首元素的地址。在主函数中可以这样计算。
 for(i=0;i<sz-1;i++)
 {
 	int flag = 1;//假设这一趟要排序的数组已经有序
 	//每一趟冒泡排序
 	int j=0;
 	for(j=0;j<sz-1-i;j++)
 	{
 		if(arr[j]>arr[j+1])
 		{
 			int tem = arr[j];
 			arr[j] = arr[j+1];
 			arr[j+1] = temp;
 			flag= 0;//本趟排序的数据不完全有序
 		}
	}
	if (flag == 1)
	{
		break;
	}
 }
}
int main()
{
	int arr[] = {9,8,7,6,5,4,3,2,1,0};
	//对arr进行排序,将结果以升序存储在数组中。
	// arr是数组,对数组arr传参,实际上传递过去的是arr的首地址,&arr[0]。
	 int sz = sizeof(arr)/sizeof(arr[0]);arr传过去的是首元素的地址。在主函数中可以这样计算。
	bubble_sort(arr,sz);
	return 0;

}

数组名是什么

#include <stdio.h>
int main(
{
	int arr[10] = {1,2,3,4,5];
	printf("%p\n",arr);//取得是数组首元素的地址
	printf("%p\n"&arr[O]);//取得是数组首元素的地址
	printf("%d\n"*arr);
	printf("%p\n"&arr);//取得是数组的地址
	//输出结果
	return 0;
}

数组名是数组首元素的地址,有两个例外:

  1. sizeof(数组名),数组名表示整个数组,sizeof(数组名)计算的是整个数组的内存大小。
  2. &数组名,数组名代表整个数组,取出的是整个数组的地址。
#include <stdio.h>
int main(
{
	int arr[10] = {1,2,3,4,567];
	printf("%p\n",arr);//取得是数组首元素的地址
	printf("%p\n",arr+1);
	
	printf("%p\n"&arr[O]);//取得是数组首元素的地址
	printf("%p\n"&arr[O]+1);
	
	printf("%p\n"&arr);//取得是数组的地址
	printf("%p\n"&arr+1);
	
	return 0;
}

在这里插入图片描述

从函数返回数组
C 语言不允许返回一个完整的数组作为函数的参数,但是,但可以通过指定不带索引的数组名来返回一个指向数组的指针。

C不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。

如果想要从函数返回一个一维数组,您必须声明一个返回指针的函数

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
  static int  r[10];
  int i;
 
  /* 设置种子 */
  srand( (unsigned)time( NULL ) );
  for ( i = 0; i < 10; ++i)
  {
     r[i] = rand();
     printf( "r[%d] = %d\n", i, r[i]);
 
  }
 
  return r;
}
 
/* 要调用上面定义函数的主函数 */
int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf( "*(p + %d) : %d\n", i, *(p + i));
   }
 
   return 0;
}

在这里插入图片描述

指向数组的指针

数组名是一个指向数组中第一个元素的常量指针。
在这里插入图片描述
在上面的声明中,arr是指向&arr[0]的指针,即数组arr的第一个元素的地址,因此p=arr是把p赋值为arr的第一个元素的地址。

使用数组名作为常量指针是合法的,反之亦然。因此*(arr+4)是访问arr[4]数据的合法方式。一旦把第一个元素的地址存储在p中,就可以使用p、(p+1)等来访问数组元素。

#include <stdio.h>
 
int main ()
{
   /* 带有 5 个元素的整型数组 */
   double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
   double *p;
   int i;
 
   p = balance;
 
   /* 输出数组中每个元素的值 */
   printf( "使用指针的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(p + %d) : %f\n",  i, *(p + i) );
   }
 
   printf( "使用 balance 作为地址的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(balance + %d) : %f\n",  i, *(balance + i) );
   }
 
   return 0;
}

在这里插入图片描述

11. C指针

C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。
在这里插入图片描述
在这里插入图片描述

  • p是一个指针,存储着变量var_runoob 的地址
  • 指针p的类型必须与变量var_runoob的类型一致,因为整型的指针只能存储整型变量的指针地址.

什么是指针?

指针也就是内存地址,指针是个变量,是用来存放内存地址的变量,存放在指针中的值都被当成地址处理。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
在这里插入图片描述
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
在这里插入图片描述
所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数,所以指针的大小只跟平台有关。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。指针类型在指针进行解引用操作的时候,能够访问空间的大小。
int* p *p能够访问4个字节
char* p *p能够访问1个字节

指针类型

在这里插入图片描述

#include <stdio.h>
int* p;
int* q;
int a = 10;

int main(void) {
    p = &a;//对变量a取出它的地址,用&操作符,将a的地址存放在p变量中,p就是一个指针变量,类型是int*
    q = a;
    printf("%d\n",*p);
    printf("%p\n",*p);
    printf("%d\n",q);
    printf("%p",q);
    return 0;
}

在这里插入图片描述

总结:

  • 指针是用来存放地址的,地址是唯一标识一块地址空间的。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。

如何使用指针

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。

这些是通过使用一元运算符*来返回位于操作数所指定地址的变量的值。
在这里插入图片描述
在这里插入图片描述

野指针

概念:野指针就是指针指问的位置是不可知的(随机的、不正确的、没有明确限制的)

  1. 指针未初始化
#include<stdio.h>

int main()
{
    int* p;
    *p = 20;
    return 0;
}
  1. 指针越界访问
#include<stdio.h>

int main()
{
    int a[10]={0};
    int i = 0;
    int* p = a;
    for(i=0;i<=12;i++)
    {
        *p = i;
        p++;
    }
    return 0;
}
  1. 指针指向的内存空间释放
#include<stdio.h>

int* test()
{
    int a = 10;//局部变量在出去函数的时候,内存会被释放
    return &a;
}
int main()
{
    int* p =test();//指针所指向的地址在调用函数结束之后被释放
    *p = 20;//指针所指向的地址内释放,因此无法赋值
    return 0;
}

调用函数参数里的值不能传递给主函数的参数,但是主函数的参数可以传递给调用函数的参数。

如何规避野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放则使之为NULL
  4. 指针使用之前检查有效性
#include <stdio.h>
int main(
{
	int *p = NULL;
	int a = 10;
	p = &a;
	if(p != NULL)
	{
		*p = 20;
	}
	return 0;
}

NULL指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。

如果将指针赋值为NULL
在这里插入图片描述
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

如需检查一个空指针,可以使用 if 语句,如下所示:
在这里插入图片描述

指针的算术运算

C 指针是一个用数值表示的地址。因此,可以对指针执行算术运算。

如果 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,ptr++ 执行完之后ptr指向1004;
如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。

总结:
指针的每一次递增,它其实会指向下一个元素的存储单元。
指针的每一次递减,它都会指向前一个元素的存储单元。
指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。

递增一个指针
我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中的数组地址 */
   ptr = var;//数组名-首元素的地址
   for ( i = 0; i < MAX; i++)
   {
 
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
 
      /* 指向下一个位置 */
      ptr++;
   }
   return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
    int a = 0x11223344;
    int* pa=&a;
    char* pc=&a;
    printf("%p \n",pa);
    printf("%p \n",pa+1);
    printf("%p \n",pc);
    printf("%p \n",pc+1);
    return 0;
}

在这里插入图片描述

指针类型决定了:指针走一步走多远(指针的步长)
int* p: p+1–> 4
char* p: p+1–> 1
double* p: p+1->8

递减一个指针

include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中最后一个元素的地址 */
   ptr = &var[MAX-1];
   for ( i = MAX; i > 0; i--)
   {
 
      printf("存储地址:var[%d] = %p\n", i-1, ptr );
      printf("存储值:var[%d] = %d\n", i-1, *ptr );
 
      /* 指向下一个位置 */
      ptr--;
   }
   return 0;
}

在这里插入图片描述
指针-指针

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
       printf("%d \n",&arr[9] - &arr[0]);
    return 0;
}

指针减去指针,得到的是元素的个数。

#include<stdio.h>

int my_strlen(char* str)
{
    char* start = str;
    char* end = str;
    while (*end != '\0')
    {
        end++;
    }
    return end - start; 
}
int main()
{
 //lstrlen -求字符串长度
 //递归–模拟实现了strlen-计数器的方式1,递归的方式2/ /
    char arr[]= "bit";
    int len = my_strlen(arr);
    printf( "%d\n", len);
    return 0;
}

在这里插入图片描述

指针的比较

指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增。

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中第一个元素的地址 */
   ptr = var;
   i = 0;
   while ( ptr <= &var[MAX - 1] ) //指针的比较
   {
 
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
 
      /* 指向上一个位置 */
      ptr++;
      i++;
   }
   return 0;
}

在这里插入图片描述
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
在这里插入图片描述
p1可以与p2进行比较,但是不允许和p3比较。

字符指针

在指针的类型中有一种指针类型为字符指针char*
一般使用:

int main()
{
	char ch = 'w';
	char *pc = &ch;
	*pc = 'w';
	return 0;
}
int main()
{
	char arr[] = "abcdef";
	char *pc = arr;
    char *p="abcdef";
    //假设是将"abcdef"字符串放到指针p里面的话,是错误的,因为char指针的大小为4,放不下字符串;此时可以这么理解,"abcdef"是一个常量字符串,把首地址“a”的地址放到指针p中。读取的时候从首地址开始读取,然后到\0停止。
	printf("%s\n",arr);
    printf("%p\n",arr);
	printf("%s\n",pc);
    printf("%p\n",pc);
    printf("%c\n",*pc);
    printf("%s\n",p);
    printf("%p\n",p);
    printf("%c\n",*p);
	return 0;
}

在这里插入图片描述
在这里插入图片描述

int main()
{
	char* p = "abcdef";
	*p = 'W';
	printf("%s\n",p);
	//Segmentation fault-段错误,一般是非法访问内存会出现。
	//"abcdef"是常量,不可以修改。可以给*p加上const。
	return 0;
}

数组指针

指向结构体的指针

#include<stdio.h>

struct T{
    int a;
    float b;
};

int main(void) {
    puts("Hello World!");
    struct T A;
    A.a = 1;
    A.b = 3.4;
    printf("%d\n", A.a);
    printf("%f\n", A.b);
    
    
    struct T p;
    // p = &A;
    // test(&p);
    
    printf("%d\n", A.a);
    printf("%f\n", A.b);
    
    printf("%d\n", p.a);
    printf("%f\n", p.b);
    return 0;
}

在这里插入图片描述

int main(void) {
    puts("Hello World!");
    struct T A;
    A.a = 1;
    A.b = 3.4;
    printf("%d\n", A.a);
    printf("%f\n", A.b);
    
    
    struct T *p;
    p = &A;
    // test(&p);
    
    printf("%d\n", A.a);
    printf("%f\n", A.b);
    
    printf("%d\n", p->a);
    printf("%f\n", p->b);
    return 0;
} 

在这里插入图片描述

指针数组

指针数组是存储指针的数组。
上述数组存储的是整数,如果我们想让数组存储执行int、char或其他数据类型的指针,则其声明为:
int *ptr[MAX] 指向整数的指针数组的声明

int main()
{
	
	int a = 10;
	int b = 20;
	int c = 30;
	//int* pa = &a;
	//int* pb = &b;
	//int* pc = &c;
	
	//整形数组–存放整形 int arr[10]={0};
	//字符数组-存放字符 char arr[10]={0};
	//指针数组–存放指针 int* arr[10]={0};整型指针的数组
	//int arr[10];
	int* arr2[3] = {&a,&b,&c};
	int i=0;
	for(i=0;i<3;i++)
	{
    	printf("%d \n",*(arr2[i]));
    }
	return 0;
}

在这里插入图片描述

#include <stdio.h>
int main()
{
	int arr1[] = { 1, 2, 3, 4, 5};
	int arr2[] = { 2, 3, 4, 5, 6};
	int arr3[] = { 3, 4, 5, 6, 7};

	int *parr[] = { arr1,arr2,arr3 };
	int i=0;
	for(i=0;i<3;i++)
	{
		for(int j=0;j<5;j++)
		{
			printf("%d ",*(parr[i]+j));
		}
		printf("\n");
    	
    }
	return 0;

}

在这里插入图片描述

在这里插入图片描述

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int i, *ptr[MAX];
 
   for ( i = 0; i < MAX; i++)
   {
      ptr[i] = &var[i]; /* 赋值为整数的地址 */
   }
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of var[%d] = %d\n", i, *ptr[i] );
   }
   return 0;
}

在这里插入图片描述

可以用一个指向字符的指针数组来存储字符串列表。

#include <stdio.h>
 
const int MAX = 4;
 
int main ()
{
   const char *names[] = {
                   "Zara Ali",
                   "Hina Ali",
                   "Nuha Ali",
                   "Sara Ali",
   };
   int i = 0;
 
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i] );
   }
   return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
    int arr[10] = { 0 };
    printf( "%p\n", arr);//地址-首元素的地址
    printf("%p\n",arr+1);
    printf("%p\n", &arr[0]);
    printf("%p\n",&arr[0]+1);
    printf("%p\n", &arr);
    printf("%p\n",&arr + 1);
}

在这里插入图片描述

#include<stdio.h>

int main()
{
    int arr[10]= { 0 };
    int* p = arr;
    int i = 0;
    for (i = 0; i< 10; i++)
    {
        *(p + i) = i;
        printf("%d ",*(p + i));
        printf("%p ==== %p \n" ,p+i, &arr[i]);
    }
}

在这里插入图片描述

数组名就是首元素的地址。
两种情况除外:
1.&arr:&数组名-数组名不是首元素的地址-数组名表示整个数组~&数组名取出的是整个数组的地址。&arr+1 跳过的是整个数组。
2. sizeof(arr) :sizeof(数组名)-数组名表示的整个数组的大小

指向指针的指针

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
在这里插入图片描述
一个指向指针的指针变量的声明必须在变量名前放置两个星号。如,声明一个指向int类型指针的指针:
int **var;

当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:
在这里插入图片描述

#include<stdio.h>
int main ()
{
   int  V;
   int  *Pt1;
   int  **Pt2;
 
   V = 100;
 
   /* 获取 V 的地址 */
   Pt1 = &V;
 
   /* 使用运算符 & 获取 Pt1 的地址 */
   Pt2 = &Pt1;
 
   /* 使用 pptr 获取值 */
   printf("var = %d\n", V );
   printf("Pt1 = %p\n", Pt1 );
   printf("*Pt1 = %d\n", *Pt1 );
   printf("Pt2 = %p\n", Pt2 );
   printf("*Pt2 = %p\n", *Pt2);
   printf("**Pt2 = %d\n", **Pt2);
 
   return 0;
}

在这里插入图片描述

传递指针给函数

C语言允许传递指针给函数,只需要简单声明函数参数为指针类型即可。

下面的实例中,我们传递一个无符号的long型指针给函数,并在函数内改变这个值。

#include <stdio.h>
#include <time.h>
void getSeconds(unsigned long *par) ;
int main ()
{
	unsigned long sec;
	getSeconds( &sec );
	/*输出实际值*/
	printf("Number of seconds : %ld\n", sec );
	return 0;
}
void getseconds(unsigned long *par)
{
	/*获取当前的秒数*/
	*par = time(NULL);
	return;
}

在这里插入图片描述
能接受指针作为参数的函数,也能接受数组作为参数,如下所示:

#include <stdio.h>
 
/* 函数声明 */
double getAverage(int *arr, int size);
 
int main ()
{
   /* 带有 5 个元素的整型数组  */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;
 
   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;
 
   /* 输出返回值  */
   printf("Average value is: %f\n", avg );
   
   return 0;
}

double getAverage(int *arr, int size)
{
  int    i, sum = 0;      
  double avg;          
 
  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }
 
  avg = (double)sum / size;
 
  return avg;
}

在这里插入图片描述

从函数返回指针(指针函数)

C语言中可以从函数返回数组,类似的,C也可以从函数返回指针。为此,必须声明一个返回指针的函数。
int * myFunction()
{

}

*声明形式:类型名 函数名(函数参数列表)
由于*的优先级低于(),所以myFunction()优先和()结合,也就意味着myFunction是一个函数,再和*结合,说明这个函数返回的是指针,前面还有int,表示myFunction是一个返回值是整型指针的函数。

另外,C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。

现在,让我们来看下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:

#include <stdio.h>
#include <time.h>
#include <stdlib.h> 
 
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
   static int  r[10];
   int i;
 
   /* 设置种子 */
   srand( (unsigned)time( NULL ) );
   for ( i = 0; i < 10; ++i)
   {
      r[i] = rand();
      printf("%d\n", r[i] );
   }
 
   return r;
}
 
/* 要调用上面定义函数的主函数 */
int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf("*(p + [%d]) : %d\n", i, *(p + i) );
   }
 
   return 0;
}

在这里插入图片描述

srand( (unsigned)time( NULL ) );
srand函数是随机数发生器的初始化函数,原型:void srand(unsigned seed)
用法:初始化随机种子,会提供一个种子,这个种子会对应一个随机数,如果使用相同的种子,后面的rand()函数会出现一样的随机数。不过为了防止随机数每次重复,常常使用系统时间来初始化。及使用time函数来获得系统时间,然后将time型数据转化为(unsigned)型再传递给srand函数,即srand((unsiged) time(&t));另一个用法就是不需要定义time型的t变量,直接 传入一个空指针,因为程序中一般不需要经过参数获得的数据。

进一步说明下:计算机并不能产生真正的随机数,而是已经编写好的一些无规则排列的数字存储在电脑里,把这些数字划分为若干相等的N份,并为每份加上一个编号用srand()函数获取这个编号,然后rand()就按顺序获取这些数字,当srand()的参数值固定的时候,rand()获得的数也是固定的,所以一般srand的参数用time(NULL),因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了。如果想在一个程序中生成随机数序列,需要至多在生成随机数之前设置一次随机种子。 即:只需在主程序开始处调用srand((unsigned)time(NULL)); 后面直接用rand就可以了。不要在for等循环放置srand((unsigned)time(NULL));

参考链接:srand((unsigned)time(NULL))详解

函数指针

函数指针是一个指向函数的指针变量,因此函数指针本身首先应该是指针变量,只不过这个指针指向的是函数。
函数名就是函数的指针,他代表函数的起始地址。可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。

声明形式:返回值类型 (*指针变量名) ([形参列表])
说明:返回值类型说明函数的返回类型,(*指针变量名)中的括号不能省略,用于改变运算符的优先级,若省略则是函数说明;“形参列表”表示指针变量指向的函数所带的参数列表。
函数指针变量的声明:
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型

int (*f) (int x) //声明一个函数指针,是一个指向返回值为int的函数的指针
f = func //将func函数的首地址赋值给指针f
f = &func //将函数地址赋值给指针f

函数指针可以像一般函数一样,用于调用函数、传递参数。

以下实例声明了函数指针变量 p,指向函数 max:

#include <stdio.h>
 
int max(int x, int y)
{
    return x > y ? x : y;
}
 
int main(void)
{
    /* p 是函数指针 */
    int (* p)(int, int) = & max; // &可以省略
    int a, b, c, d;
 
    printf("请输入三个数字:");
    scanf("%d %d %d", & a, & b, & c);
 
    /* 与直接调用函数等价,d = max(max(a, b), c) */
    d = p(p(a, b), c); 
 
    printf("最大的数字是: %d\n", d);
 
    return 0;
}

在这里插入图片描述

函数指针可以作为参数和返回值
#include <stdio.h>

int add(int num1,int num2)
{
	return num1+num2;
}

int sub(int num1,int num2)
{
	return num1-num2;
}

int fun(int (*fp)(int,int),int num1,int num2)//函数指针做参数
{
	return (*fp)(num1,num2);
}

int (*select(char c))(int,int) //函数指针作为返回值,等同于typedef int(*PF)(int,int);PF select(char c);这种表达方式最好。
{
	switch(c)
	{
		case '+':return add;
		case '-':return sub;
	}
}

int main()
{
	int num1,op,num2;
	int (*fp)(int,int);
	printf("请输入一个表达式,比如1+3:\n");
	scanf("%d%c%d",&num1,&op,&num2);
	fp = select(op);
	printf("%d%c%d=%d",num1,op,num2,fun(fp,num1,num2))
	return 0;

}

说明:

  1. int (*select(char c))(int,int)分解
  • select与后面的(char c)结合,说明是一个函数,即select(char c)
  • 再和*结合,说明select函数的返回值是一个指针,即*(select(char c))
  • 再和后面的(int,int)结合,说明select函数返回的指针指向函数,不是指向int类型,即int (*select(char c))(int,int)。
  • 总结:返回值为select函数指针,该返回值指向一个 返回值是int,两个参数为int类型 的函数。
使用typedef关键字

funcPtr是一个函数指针变量,用于指向返回值为int,一个int类型参数的函数。

typedef int (*PF) (int,int);

当使用typedef声明后,则funcPtr就成为了一个函数指针类型,即 typedef int (*PF)(int *, int); 这样就定义了返回值的类型。可以指向一个接受两个int类型参数并返回int类型结果的函数。可以这样使用函数指针:

int add(int a, int b) {
	return a + b;
}
int main() {
	PF ptr = add; //函数指针ptr指向add函数
	int result = ptr(23);//调用add函数,result的值为5
	return 0;

再用funcPtr作为返回值来声明函数:PF func(int); // func(int)就是一个返回值为函数指针,一个int类型参数的函数
再用PF来声明:PF phead; //phead就是一个函数指针

指针函数和函数指针

  1. 指针函数本质是一个函数,不过返回值是指针。
  2. 函数指针是指向函数的指针变量。 因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。

回调函数

函数指针作为某个函数的参数。

函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

简单讲:回调函数是由别人的函数执行时调用你实现的函数。

#include <stdlib.h>  
#include <stdio.h>
 
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}
 
// 获取随机值
int getNextRandomValue(void)
{
    return rand();
}
 
int main(void)
{
    int myarray[10];
    /* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
        printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
}

在这里插入图片描述
实例中 populate_array() 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。

实例中我们定义了回调函数 getNextRandomValue(),它返回一个随机值,它作为一个函数指针传递给 populate_array() 函数。

populate_array() 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。

函数指针数组

指向函数指针数组的指针

指针和数组经典面试题

1
#include <stdio.h>
int main()
{
    char str1[] = "he11o bit.";
    char str2[] = "he11o bit.";
    char *str3 = "he11o bit.";
    char *str4 = "he11o bit.";
    printf("%s\n",str1);//str1放到是首元素的地址。初始化不同的数组的时候会开辟出布偶听的内存块。
    printf("%s\n",str1);
    printf("%s\n",str3);//str3指向同一个常量的首地址,常量是单独存储在常量区的,指向同一个字符串的时候,实际指向的是同一块内存,准确的写法应该加上const
    printf("%s\n",str4);
    printf("%p\n",str1);
    printf("%p\n",str1);
    printf("%p\n",str3);
    printf("%p\n",str4);
    if(str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");
    if(str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");
    return 0;
}

在这里插入图片描述

13. 字符串

在 C 语言中,字符串实际上是使用空字符\0结尾的一维字符数组。因此,\0 是用于标记字符串的结束。

空字符(Null character)又称结束符,缩写 NUL,是一个数值为0的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。

下面的声明和初始化创建了一个 RUNOOB 字符串。由于在数组的末尾存储了空字符 \0,所以字符数组的大小比单词 RUNOOB 的字符数多一个。

在这里插入图片描述依据数组初始化规则,可以写为
在这里插入图片描述
字符串的内存表示:
在这里插入图片描述
其实,我们不需要把 null 字符放在字符串常量的末尾。C 编译器会在初始化数组时,自动把 \0 放在字符串的末尾。让我们尝试输出上面的字符串:

14. 结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许存储不同类型的数据项。结构可用于表示一条记录,如想跟踪图书馆书本的动态,可能需要跟踪多个属性,如:id,author,book id等等。

结构体的声明

为了定义结构,您必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:
在这里插入图片描述
tag 是结构体标签。

member-list 成员列表,是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。

variable-list 变量列表,结构变量,定义在结构的末尾,最后一个分号之前,可以指定一个或多个结构变量,此处的变量是全局的结构体变量。下面是声明 Book 结构的方式:

在这里插入图片描述
在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。以下为实例:
在这里插入图片描述
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

在这里插入图片描述
如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:
在这里插入图片描述

结构体变量的初始化

和其它类型变量一样,对结构体变量可以在定义时指定初始值。

在这里插入图片描述
在这里插入图片描述

访问结构成员

为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。您可以使用 struct 关键字来定义结构类型的变量。

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
int main( )
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;
 
   /* Book2 详述 */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* 输出 Book1 信息 */
   printf( "Book 1 title : %s\n", Book1.title);
   printf( "Book 1 author : %s\n", Book1.author);
   printf( "Book 1 subject : %s\n", Book1.subject);
   printf( "Book 1 book_id : %d\n", Book1.book_id);
 
   /* 输出 Book2 信息 */
   printf( "Book 2 title : %s\n", Book2.title);
   printf( "Book 2 author : %s\n", Book2.author);
   printf( "Book 2 subject : %s\n", Book2.subject);
   printf( "Book 2 book_id : %d\n", Book2.book_id);
 
   return 0;
}

在这里插入图片描述

结构体内存对齐

计算结构体的大小,首先得掌握结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
vs中默认的值为8
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
5.linux的gcc编译器不存在默认对齐数的概念

#include<stdio.h>

struct S1
{
    char c1;
    int a;
    char c2;
};
struct S2
{
    char c1;
    char c2;
    int a;
};
struct S3
{
    double d;
    char c;
    int i;
};
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
int main()
{
    struct S1 s1 = {0};
    printf("%d\n", sizeof(s1));
    struct S2 s2= {0};
    printf("%d\n", sizeof(s2));
    struct S3 s3= {0};
    printf("%d\n", sizeof(s3));
    struct S4 s4= {0};
    printf("%d\n", sizeof(s4));
    return 0;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
为什么存在内存对齐?
大部分的参考资料都是如是说的:

  1. 平台原因(移植原因)︰不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    总体来说∶
    结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

  1. 让占用资间小的成员尽量集中在一起。s1写成s2的形式。
  2. 修改默认对齐数。结构在对齐方式不合适的时候,我们可以目己更改默认对齐数。

预处理指令 #pragma pack(8) 设置默认对齐数为8。
#pragma pack() 取消修改默认对齐数。

#include<stdio.h>
#include<stddef.h>

struct S
{
    char c;
    int a;
    double d;
};

int main()
{
    //offsetof() 结构体成员相对于首地址的偏移量
    printf("%d\n", offsetof(struct S,c));
    printf("%d\n", offsetof(struct S,a));
    printf("%d\n", offsetof(struct S,d));
   
    return 0;
}

在这里插入图片描述

结构体作为函数参数

您可以把结构体作为函数参数,传参方式与其他类型的变量或指针类似。

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
/* 函数声明 */
void printBook( struct Books book );
int main( )
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;
 
   /* Book2 详述 */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* 输出 Book1 信息 */
   printBook1( Book1 );
 
   /* 输出 Book2 信息 */
   printBook2( &Book2 );
 
   return 0;
}
void printBook1( struct Books book )
{
   printf( "Book title : %s\n", book.title);
   printf( "Book author : %s\n", book.author);
   printf( "Book subject : %s\n", book.subject);
   printf( "Book book_id : %d\n", book.book_id);
}
void printBook2( struct Books* ps )
{
   printf( "Book title : %s\n", ps->title);
   printf( "Book author : %s\n", ps->author);
   printf( "Book subject : %s\n", ps->subject);
   printf( "Book book_id : %d\n", ps->book_id);
}

在这里插入图片描述

#include<stdio.h>

struct S
{
    char c;
    int a;
    double d;
};
/*
void Init(struct S tmp)//传值,错误
{
    tmp.a = 100;
    tmp.c = 'w';
    tmp.d = 3.14;
}
*/
void Init(struct S* ps)//传地址
{
    ps->a = 100;
    ps->c = 'w';
    ps->d = 3.14;
}
void Print1(struct S tmp)
{
	printf("%d %c %1f\n",tmp.a,tmp.c,tmp.d);
}
void Print1(const struct S* ps)
{
	printf("%d %c %1f\n",ps->a,ps->c,ps->d);
}
int main()
{
    struct S s={0};
    //Init(s);
    Init(&s);
    Print1(s);
    return 0;
}

结构体s直传进去,tmp是s的copy,tmp和s的地址不同,所以在Init函数中给tmp赋值是无效的,s的值不会被修改,因此要赋值传入的应该是指针,传入s的地址。函数内部想要修改函数外部的内容,只能把地址传进去。
但是Print中只是访问,因此可以直传。但是实际中最好使用指针形式,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降;而指针大小是固定的,在结构体较大时,能节省空间。为防止指针指向的变量内容被改变,可以加const防止内容被修改。

总结:结构体传参要传结构体的地址

压栈

#include <stdio.h>
 
int Add(int x,int y)
{
    int z = 0;
    z=x+y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = Add(a, b);
    return 0;
}

在这里插入图片描述
放一个元素进去的动作叫做压栈操作,

指向结构的指针

您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似。

struct Books *struct_pointer;

现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:

struct_pointer = &Book1;

为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:

struct_pointer->title;

实例:

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
/* 函数声明 */
void printBook( struct Books *book );
int main( )
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;
 
   /* Book2 详述 */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* 通过传 Book1 的地址来输出 Book1 信息 */
   printBook( &Book1 );
 
   /* 通过传 Book2 的地址来输出 Book2 信息 */
   printBook( &Book2 );
 
   return 0;
}
void printBook( struct Books *book )
{
   printf( "Book title : %s\n", book->title);
   printf( "Book author : %s\n", book->author);
   printf( "Book subject : %s\n", book->subject);
   printf( "Book book_id : %d\n", book->book_id);
}

位段/位域

什么是位段
位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是int. unsigned int 或signed int。
  2. 位段的成员名后边有一个冒号和一个数字。

如struct test
{
int a :1;
int b :1;
int c :5;
int d :10;
};
这里不是给a赋初值,在内存中存取数据的最小单位一般是字节,但有时存储一个数据不必用一个字节。
这是一种位域的结构体,这个结构里a占用的是一个字节中的1位,b也占用1位.所以这里的a和b的取值只能是0和1。因为它们都是用1位来表示的。

#include<stdio.h>

struct S
{
	int a :2;
	int b :5;
	int c :10;
	int d :30;
};
int main()
{
    struct S s={0};
    printf("%d \n",sizeof(s));
    return 0;
}

在这里插入图片描述
位段的内存分配

  1. 位段的成员可以是int unsigned int signed int或者是char(属于整型家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节(char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
    在这里插入图片描述
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/cef04441bd68482cbe82ae367e7a65d1.pn
    在这里插入图片描述

位段的跨平台问题

  1. Int位段被当成有符号数还是无符号数是不确定的.
  2. 位段中最大位的数目不能确定。( 16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时﹐是舍弃剩余的位还是利用,这是不确定的。

总结:
跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在。

位段的应用
在这里插入图片描述

15. 共用体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

定义共用体

为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
在这里插入图片描述
union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:

在这里插入图片描述

共用体的大小

  1. 共用体占用的内存应足够存储共用体中最大的成员。
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
    例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。下面的实例将显示上面的共用体占用的总内存大小:
#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   printf( "Memory size occupied by data : %d\n", sizeof(data));
 
   return 0;
}

在这里插入图片描述

访问共用体成员

为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}

在这里插入图片描述
在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。

现在让我们再来看一个相同的实例,这次在同一时间只使用一个变量,这也演示了使用共用体的主要目的:

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   printf( "data.i : %d\n", data.i);
   
   data.f = 220.5;
   printf( "data.f : %f\n", data.f);
   
   strcpy( data.str, "C Programming");
   printf( "data.str : %s\n", data.str);
 
   return 0;
}

在这里插入图片描述
第一种

int main()
{
	int a = 1;
	if (1 == *(char*)&a)
	{
		printf("小端\n");
    }
    else
    {
    	printf("大端\n");
    }
	
	//int a = 0x11 22 33 44;
	// 低地址--------->高地址
	// [][11][22][33][44][]大端字节序存储模式
	// [][44][33][22][11][]小端字节序存储模式
   //讨论一个数据,放在内存中的存放的字节顾序
   //大小端字节序问题
	return 0;
 }

第二种

#include<stdio.h>

int check_sys()
{
    int a = 1;
    //返回1,表示小端
    //返回0,表示大端
    return *(char*)&a;
}

int main()
{
    int ret=check_sys();
    if (1 == ret)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
 }

第三种

#include<stdio.h>

int check_sys()
{
    union
    {
        char c;
        int i;
    }u;
    u.i=1;
    return u.c;
}

int main()
{
    int ret=check_sys();
    if (1 == ret)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
 }

16. C typedef

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。

typedef type newname

下面的实例为单字节数字定义了一个术语 BYTE:
在这里插入图片描述
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:
在这里插入图片描述
您也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。例如,您可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如下:

#include <stdio.h>
#include <string.h>
 
typedef struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} Book;
 
int main( )
{
   Book book;
 
   strcpy( book.title, "C 教程");
   strcpy( book.author, "Runoob"); 
   strcpy( book.subject, "编程语言");
   book.book_id = 12345;
 
   printf( "书标题 : %s\n", book.title);
   printf( "书作者 : %s\n", book.author);
   printf( "书类目 : %s\n", book.subject);
   printf( "书 ID : %d\n", book.book_id);
 
   return 0;
}

在这里插入图片描述

typedef 和 #define

define 与typedef大体功能都是使用时给一个对象取一个别名,增强程序的可读性,但它们在使用时有以下几点区别:

  1. 定义不一样
    define定义后面不用加分号,并且它的别名在对象的前面
    typedef需要加分号,并且它的别后面替换对象的前面
#define AMX(x,y) ((x)>(y)?(x):(y)) //加括号提升优先级,防止传入数据时出现错误
typedef signed int S32;
  1. 原理不一样
    define是预处理中的宏定义命令,在预处理时会进行字符串的替换,且不做正确性检查,只有编译已被展开的源代码时才会发现可能的错误并报错。
    typedef是关键字,在编译时,有类似功能检查的作用,它在自己的 作用域内给已有的数据类型一个别名,一般用来定义数组、指针、结构体等,也能为数值定义别名,比如您可以定义 1 为 ONE;
    typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
    在这里插入图片描述
    在这里插入图片描述
    可以看出使用typedef定义的两个变量都是int指针类型,而使用define定义的两个变量只有第一个是int指针类型。
  2. 作用域不同
    #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,因此使用#define之后为了防止错误,要将其解除掉。但是typedef有自己的作用域。
#include <stdio.h>
 
#define TRUE  1
#define FALSE 0
 
int main( )
{
   printf( "TRUE 的值: %d\n", TRUE);
   printf( "FALSE 的值: %d\n", FALSE);
 
   return 0;
}

参考链接:typedef和define

define高级用法 #define __FUNCTION__ __FILE__

我们在写程序的时候,总是或多或少会加入一些printf之类的语句用于输出调试信息,但是printf语句有个很不方便的地方就是当我们需要发布程序的时候要一条一条的把这些语句删除,而一旦需要再次调试的时候,这些语句又不得不一条条的加上,这给我们带来了很大的不便,浪费了我们很多的时间,也造成了调试的效率低下。所以,很多人会选择使用宏定义的方式来输出调试语句。

比如,定义一个宏开关:
#define __DEBUG
当需要调试的时候,使用语句:
#ifdef __DEBUG
printf(xxx);
#endif

这种方式的调试,可以通过undef __DEBUG的方式让告知编译器不编译这些语句,从而不再输出这些语句。但是这种方式的麻烦之处也是显而易见的,每一条调试语句都需要使用两条宏定义来包围,这不但在代码的编写上不便,源码结构也不好看,工作量依然不小。

如果我们能够把这三条语句编程一条,那该多舒服呀~,于是,我们想到使用这样的语句:

#ifdef __DEBUG
#define DEBUG(info)    printf(info)
#else
#define DEBUG(info)
#endif

这样,我们在编写代码的时候,使用DEBUG一条语句就可以了,我们把宏开关__DEBUG打开,所有的DEBUG(info)宏定义信息都会被替换为printf(info),关上则会被替换成空,因此不会被编译。嗯,这次方便多了,一条语句就可以了~~~ 但是,问题也随之而来了,printf是支持多个参数的,而且是不定参数,当你使用下面这样的语句时就会报错:

DEBUG(“%s”,msg)

这是因为,DEBUG(info)这条宏定义只支持一个参数的替换。

因此,我们希望DEBUG能够像printf那样,支持多个参数,并且这些参数刚好展开成为printf语句本身使用的参数。

#define DEBUG(format, ...) printf (format, ##__VA_ARGS__)(’ ## '的意思是,如果可变参数被忽略或为空,将使预处理器( preprocessor )去除掉它前面的那个逗号。)

于是乎,我们神奇地发现,DEBUG完全取代了printf,所有的DEBUG(…)都被完成的替换成了printf(…),再也不会因那个可恶的逗号而烦恼了。

但是,我们发现,光有printf还不够,虽然调试信息是输出了,可是很多的调试信息输出,我们并不能一下子知道这条信息到底是在那里打印出来的,于是,我们又想,能不能把当前所在文件名和源码行位置也打印出来呢,这样不就一目了然了吗,哪里还用的着去想,去找调试信息在哪里输出的呢,都已经打印出来了!

于是就有了编译内置宏

先介绍几个编译器内置的宏定义,这些宏定义不仅可以帮助我们完成跨平台的源码编写,灵活使用也可以巧妙地帮我们输出非常有用的调试信息。

ANSI C标准中有几个标准预定义宏(也是常用的):

__LINE__:在源代码中插入当前源代码行号;
__FILE__:在源文件中插入当前源文件名;
__DATE__:在源文件中插入当前的编译日期
__TIME__:在源文件中插入当前编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义。

编译器在进行源码编译的时候,会自动将这些宏替换为相应内容。

看到这里,你的眼睛应该一亮了吧,嗯,是的,__FILE__和__LINE__正是我们前面想要的输出的,于是,我们的每一条语句都变成了:

DEBUG("FILE: %s, LINE: %d…",__FILE__,__LINE__,…)

其实没有必要,__FILE__本身就会被编译器置换为字符常量,于是乎我们的语句又变成了这样:

DEBUG(“FILE:”__FILE__“, LINE: %d…”,__LINE__,…)

但是,我们还是不满足,依然发现,还是很讨厌,为什么每条语句都要写"FILE:“__FILE__”, LINE: %d 以及,__LINE__,这两个部分呢?这不是浪费我们时间么?

哈哈,是的,这就是本次大结局,把DEBUG写成这样:

#define DEBUG(format,...) kprintf("File: "__FILE__"(%05d)[FUN:"__FUNCTION__"]:"format"\n", __LINE__, ##__VA_ARGS__)

没错,就是这样!下面,所有的DEBUG信息都会按照这样的方式输出:

FILE: xxx, LINE: xxx, …….

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

原文链接:#define 高级用法(Ex) FILE FUNCTION __LINE

C输入&输出

当我们提到输入时,这意味着要向程序填充一些数据。输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入,并根据需要填充到程序中。

当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C 语言提供了一系列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。

标准文件

C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
在这里插入图片描述
文件指针是访问文件的方式,接下俩介绍如何从屏幕读取数值及如何把结果输出到屏幕上。
C语言的I/O(输入和输出)通常使用printf()和scanf()两个函数。
scanf() 函数用于从标准输入(键盘)读取并格式化, printf() 函数发送格式化输出到标准输出(屏幕)。

#include <stdio.h>      // 执行 printf() 函数需要该库
int main()
{
    printf("菜鸟教程");  //显示引号中的内容
    return 0;
}

在这里插入图片描述
实例解析:

  • 所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
  • printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。
  • stdio.h 是一个头文件 (标准输入输出头文件) and #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。
  • return 0; 语句用于表示退出程序。

%d格式化输出整数

#include <stdio.h>
int main()
{
    int testInteger = 5;
    printf("Number = %d", testInteger);
    return 0;
}

在这里插入图片描述
在 printf() 函数的引号中使用 “%d” (整型) 来匹配整型变量 testInteger 并输出到屏幕。

%f 格式化输出浮点型数据

#include <stdio.h>
int main()
{
    float f;
    printf("Enter a number: ");
    // %f 匹配浮点型数据
    scanf("%f",&f);
    printf("Value = %f", f);
    return 0;
}

getchar() & putchar() 函数

int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。可以在循环内使用这个方法,以便从屏幕上读取多个字符。

int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。可以在循环内使用这个方法,以便在屏幕上输出多个字符。

#include <stdio.h>
 
int main( )
{
   int c;
 
   printf( "Enter a value :");
   c = getchar( );
 
   printf( "\nYou entered: ");
   putchar( c );
   printf( "\n");
   return 0;
}

当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并只会读取一个单一的字符,显示如下:
在这里插入图片描述

gets()&puts()函数

char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

#include <stdio.h>
 
int main( )
{
   char str[100];
 
   printf( "Enter a value :");
   gets( str );
 
   printf( "\nYou entered: ");
   puts( str );
   return 0;
}

当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取一整行直到该行结束,显示如下:
在这里插入图片描述

scanf()和printf()函数

int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。

#include <stdio.h>
int main( ) {
 
   char str[100];
   int i;
 
   printf( "Enter a value :");
   scanf("%s %d", str, &i);
 
   printf( "\nYou entered: %s %d ", str, i);
   printf("\n");
   return 0;
}

当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取输入,显示如下:
在这里插入图片描述
在这里,应当指出的是,scanf() 期待输入的格式与您给出的 %s 和 %d 相同,这意味着您必须提供有效的输入,比如 “string integer”,如果您提供的是 “string string” 或 “integer integer”,它会被认为是错误的输入。另外,在读取字符串时,只要遇到一个空格,scanf() 就会停止读取,所以 “this is test” 对 scanf() 来说是三个字符串。

C文件读写

C程序如何创建、打开、关闭文本文件或二进制文件。
一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。本章将讲解文件管理的重要调用。

打开文件 fopen()

可以使用fopen()函数创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:

FILE *fopen( const char *filename, const char *mode );

filename:字符串,用来命名文件;
mode:访问模式,可以为下列值的一个。

在这里插入图片描述
如果是二进制文件,则需使用下面的访问模式来取代上面的访问模式:
"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

关闭文件 fclose()

为了关闭文件,请使用 fclose( ) 函数。函数的原型如下:
int fclose( FILE *fp );
如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

写入文件 fputc()

把字符写入到流中的最简单的函数:

int fputc( int c, FILE *fp );

函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:

int fputs( const char *s, FILE *fp );

函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。也可以使用 int fprintf(FILE *fp,const char *format, …) 函数把一个字符串写入到文件中。尝试下面的实例:

注意:请确保您有可用的 tmp 目录,如果不存在该目录,则需要在您的计算机上先创建该目录。
/tmp 一般是 Linux 系统上的临时目录,如果你在 Windows 系统上运行,则需要修改为本地环境中已存在的目录,例如: C:\tmp、D:\tmp等。
#include <stdio.h>
 
int main()
{
   FILE *fp = NULL;
 
   fp = fopen("/tmp/test.txt", "w+");
   fprintf(fp, "This is testing for fprintf...\n");
   fputs("This is testing for fputs...\n", fp);
   fclose(fp);
}

当上面的代码被编译和执行时,它会在 /tmp 目录中创建一个新的文件 test.txt,并使用两个不同的函数写入两行。接下来让我们来读取这个文件。

读取文件 fgetc()

下面是从文件中读取单个字符最简单的函数:

int fgetc(FILE * fp);

fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。下面的函数能让我们从流中读取一个字符串:

char *fgets( char *buf, int n, FILE *fp );

函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。您也可以使用 int fscanf(FILE *fp, const char *format, …) 函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。

#include <stdio.h>

int main()
{
	FILE *fp = NULL;
	char buff[255];
	fp = fopen("/tmp/test.txt","r");
	fscanf(fp,"%s",buff);
	print("1:%s\n",buff);
	
	fgets(buff,255,(FILE*)fp);
	print("2:%s\n",buff);
	
	fgets(buff,255,(FILE*)fp);
	print("3:%s\n",buff);
	fclose(fp);
}

在这里插入图片描述
首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。其次,调用 fgets() 读取剩余的部分,直到行尾。最后,调用 fgets() 完整地读取第二行。

二进制I/O函数

下面两个函数用于二进制输入和输出:

size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

这两个函数都是用于存储块的读写,通常是数组或结构体。

C 预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

在这里插入图片描述

预处理器实例

#define MAX_ARRAY_LENGTH 20
这个指令告诉C预处理器把所有的MAX_ARRAY_LENGTH定会一为20。使用#difine定义常量增强可读性。

#include <stdio.h>
#include "myheader.h"

第一个指令告诉C预处理器从系统库中获取stdio.h,并添加文本到当前的源文件中。第二个指令是从本地目录中获取myheader.h,并添加内容到当前的源文件中。

#undef  FILE_SIZE
#define FILE_SIZE 42

这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#ifdef DEBUG
   /* Your debugging statements here */
#endif

这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。

预定义宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
在这里插入图片描述

#include <stdio.h>
 
main()
{
   printf("File :%s\n", __FILE__ );
   printf("Date :%s\n", __DATE__ );
   printf("Time :%s\n", __TIME__ );
   printf("Line :%d\n", __LINE__ );
   printf("ANSI :%d\n", __STDC__ );
 
}

在这里插入图片描述

预处理器运算符

宏延续运算符(\)

一个宏通常写在一个单行上,但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。
例如:

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。

#include <stdio.h>
 
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
 
int main(void)
{
   message_for(Carole, Debra);
   return 0;
}
标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。

#include <stdio.h>
 
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

在这里插入图片描述
token##n合并起来为为token34,token34在主函数中定义为40,token##n 会连接到 token34 中。

#include <stdio.h>
 
#define tokenpaster(n)\
  printf ("token" #n " = %d", n)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

在这里插入图片描述

defined()运算符

预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

#include <stdio.h>
 
#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif

/*与上面等价,上面是将未定义写为函数形式
#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif
*/

int main(void)
{
   printf("Here is the message: %s\n", MESSAGE);  
   return 0;
}

在这里插入图片描述

参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。

int square(int x) {
   return x * x;
}

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x))

C头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

引用头文件的语法

使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:

#include <file>
这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

#include "file"
这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

引用头文件的操作

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 header.h,如下:
在这里插入图片描述

只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
在这里插入图片描述

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:

在这里插入图片描述
但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,您只需要使用宏名称代替即可:
在这里插入图片描述
SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。
在这里插入图片描述

C强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:
在这里插入图片描述
使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:

#include <stdio.h>
 
int main()
{
   int sum = 17, count = 5;
   double mean;
 
   mean = (double) sum / count;
   printf("Value of mean : %f\n", mean );
}

这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。

类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。

整数提升

整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。

#include <stdio.h>
 
int main()
{
   int  i = 17;
   char c = 'c'; /* ascii 值是 99 */
   int sum;
 
   sum = i + c;
   printf("Value of sum : %d\n", sum );
 
}

在这里插入图片描述
sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 ‘c’ 的值转换为对应的 ascii 值。

常用的算数转换

常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:

在这里插入图片描述
常用的算术转换不适用于赋值运算符、逻辑运算符 && 和 ||。

#include <stdio.h>
 
int main()
{
   int  i = 17;
   char c = 'c'; /* ascii 值是 99 */
   float sum;
 
   sum = i + c;
   printf("Value of sum : %f\n", sum );
 
}

在这里插入图片描述
c 首先被转换为整数,但是由于最后的值是 float 型的,所以会应用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

错误处理

头文件 : #include <errno.h>

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,它是一个返回值,会显示相关错误的文本消息。 该错误代码是全局变量,表示在函数调用期间发生了错误。所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

errno、perror() 和 strerror()

C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息

perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
让我们来模拟一种错误情况,尝试打开一个不存在的文件。您可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,您应该使用 stderr 文件流来输出所有的错误。

#include <stdio.h>
#include <errno.h>
#include <string.h>
 
extern int errno ;
 
int main ()
{
   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");
   if (pf == NULL)
   {
      errnum = errno;
      fprintf(stderr, "错误号: %d\n", errno);
      perror("通过 perror 输出错误");
      fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
   }
   else
   {
      fclose (pf);
   }
   return 0;
}

在这里插入图片描述

被零除的错误

在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。

为了避免这种情况发生,在进行除法运算前要先检查除数是否为零:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
   int dividend = 20;
   int divisor = 0;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(-1);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为 : %d\n", quotient );
 
   exit(0);
}

在这里插入图片描述

程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。

#include <stdio.h>
#include <stdlib.h>
 
main()
{
   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(EXIT_FAILURE);
   }
   quotient = dividend / divisor;
   fprintf(stderr, "quotient 变量的值为: %d\n", quotient );
 
   exit(EXIT_SUCCESS);
}

在这里插入图片描述

内存管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

在这里插入图片描述
注意:void* 类型表示未确定类型的指针。C、C++ 规定 void* 类型可以通过类型转换强制转换为任何其它类型的指针。

动态分配内存

编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

char name[100];
但是,如果您预先不知道需要存储的文本长度,例如您想存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 200 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student in class 10th");
   }
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
}

在这里插入图片描述

重新调整内存的大小和释放内存

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。
或者,可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 30 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student.");
   }
   /* 假设您想要存储更大的描述信息 */
   description = (char *) realloc( description, 100 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
 
   /* 使用 free() 函数释放内存 */
   free(description);
}

在这里插入图片描述

静态库和动态库

库(Library)是一段编译好了的二进制代码,加上头文件就可以供别人使用。

什么时候我们会用到库呢?一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。

Link 的方式有两种,静态和动态,于是便产生了静态库和动态库。

静态库:
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。

优势:编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。

  1. 模块化,分工合作;
  2. 避免少量改动经常导致大量的重复编译;
  3. 可以重用,不是共享使用。

缺点:会使用目标程序的体积增大。

动态库:
动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。

优势:

  1. 不需要拷贝到目标程序中,不会影响目标程序的体积
  2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。
  3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。

缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)。

参考链接:静态库和动态库的区别

嵌入式C语言

字符串函数

常用函数

在这里插入图片描述

#include <stdio.h>
#include <string.h>
 
int main ()
{
   char str1[14] = "runoob";
   char str2[14] = "google";
   char str3[14];
   int  len ;
 
   /* 复制 str1 到 str3 */
   strcpy(str3, str1);
   printf("strcpy( str3, str1) :  %s\n", str3 );
 
   /* 连接 str1 和 str2 */
   strcat( str1, str2);
   printf("strcat( str1, str2):   %s\n", str1 );
 
   /* 连接后,str1 的总长度 */
   len = strlen(str1);
   printf("strlen(str1) :  %d\n", len );
 
   return 0;
}

在这里插入图片描述

memset

memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。

void *memset(void *s, int c, size_t n); 
  • s指向要填充的内存块。
  • c是要被设置的值。
  • n是要被设置该值的字符数。
  • 返回类型是一个指向存储区s的指针。
memmove

memmove(void *dst, void *src, size_t num)

  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。C语言规定memmove可以重叠拷贝。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
memcpy

memmove(void *dst, void *src, size_t num)
内存复制。

  • 函数memcpy从src的位置开始向后复制num个字节的数据到dest的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果src和dest有任何的重叠,复制的结果都是未定义的。
  • 拷贝的时候应该是不重叠的拷贝。
int main()
{
   int arr1[10] = { 0,1,2,3,4,5,6,7,8,9 };
   int arr2[5];

   memcpy(arr2, arr1, sizeof(arr1[0]) * 5);

   for (int i = 0; i < 5; i++)
   {
   	printf("%d", arr2[i]);
   }
   //结果为:01234

   return 0;
}
strlen

strlen(s1)
返回字符串s1的长度。

strchr

strchr(s1, ch)
返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。

strstr

返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

strcpy

复制字符串,char *strcpy(char *dest, const char *src),表示把src所指向的字符串复制到dest

#include <stdio.h>
#include <string.h>
 
int main ()
{
   char str1[14] = "runoob";
   char str2[14] = "google";
   char str3[14];
   int  len ;
 
   /* 复制 str1 到 str3 */
   strcpy(str3, str1);
   printf("strcpy( str3, str1) :  %s\n", str3 );
 
   /* 连接 str1 和 str2 */
   strcat( str1, str2);
   printf("strcat( str1, str2):   %s\n", str1 );
 
   /* 连接后,str1 的总长度 */
   len = strlen(str1);
   printf("strlen(str1) :  %d\n", len );
 
   return 0;
}

在这里插入图片描述

STUFF

替换字符串

返回使用其他字符串替换指定字符表达式中指定数量的字符的字符串。

STUFF(cExpression,nStartReplacement,nCharactersReplaced,cReplacement)

cExpression 指定要在其中进行替换的字符表达式;

nStartReplacement 指定cExpression中开始替换的位置;

nCharactersReplaced 指定要替换的字符数,如果为0,则替换串cReplacement被插入到cExpression中。

CReplacement 指定用以替换的字符串表达式,如果为空,则从cExpression中移除nCharactersReplaced指定的字符数。

string1 = “abcdefg”
string2 = “12345”

STUFF(string1,4,0,string2) //插入
STUFF(string1,4,3,string2) // 替换
STUFF(string1,4,6,‘’) //删除
参考链接:STUFF函数

strcmp

比较字符串的大小。

把字符串str1和str2从首字符开始逐个字符的进行比较,直到某个字符不相同或者其中一个字符串比较完毕才停止比较。字符的比较为ASCII码的比较。

int strcmp(char *str1,char *str2);
若字符串1大于字符串2,返回结果大于零;若字符串1小于字符串2,返回结果小于零;若字符串1等于字符串2,返回结果等于零。

参考链接:strcmp函数

strcat

strcat(str,ptr) 将字符串ptr的内容连接到str之后。
str=“123456\0”;
ptr = “abc\0”;
strcat(str,ptr)后str内容为“123456abc\0”。

如果修改str[1] = ‘\0’,str =“1\03456\0”,结果变为“1abc\06\0”,但printf只会打印打印到\0,后面就被遗弃了,变为“1abc”

系统函数

srand

srand((unsigned)time(NULL))是初始化随机函数种子:

1、是拿当前系统时间作为种子,由于时间是变化的,种子变化,可以产生不相同的随机数。计算机中的随机数实际上都不是真正的随机数,如果两次给的种子一样,是会生成同样的随机序列的。 所以,一般都会以当前的时间作为种子来生成随机数,这样更加的随机。
2、使用时,参数可以是unsigned型的任意数据,比如srand(10);
3、如果不使用srand,用rand()产生的随机数,在多次运行,结果是一样的

常见C语言编译错误

  1. assignment makes pointer from integer without a cast(C语言头文件)
    原则:在源文件中没有发现函数的声明,可能是忘加头文件了。(未经声明的函数原型一律默认为返回int值)

参考链接:assignment makes pointer from integer without a cast(C语言头文件)

  1. [Warning] large integer implicitly truncated to unsigned type [-Woverflow]

警告的原因是:整数溢出
整数溢出:当整数达到它所能表述的最大值时,会重新从起点开始

参考链接:[Warning] large integer implicitly truncated to unsigned type [-Woverflow]

实用调试技巧

什么是bug ?

在这里插入图片描述

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。

调试是什么?有多重要?

调试(英语:Debugging / Debug ),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

调试的基本步骤

  1. 发现程序错误的存在
  2. 以隔离、消除等方式对错误进行定位确定错误产生的原因
  3. 提出纠正错误的解决办法
  4. 对程序错误予以改正,重新测试

debug和release的介绍。

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

windows环境调试介绍。

  1. vs环境
    在这里插入图片描述
  2. 快捷键

F5
启动调试.经常用来直接调到下-一个断点处。
F9
创建断点和取消断点。断点的重要作用,可以在程序的任意位置设置断点,这样就可以使得程序在想要的位置随意停止执行,继而步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。
CTRL+ F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

  1. 调试的时候查看程序当前信息
    查看临时变量的值
    调试->窗口->自动窗口、局部变量、监视、内存、反汇编、调用堆栈

调用堆栈在这里插入图片描述
4. 多多动手,尝试调试,才能有进步。
一定要熟练掌握调试技巧。
初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试
我们所讲的都是一些简单的调试。以后可能会出现很复杂调试场景:多线程程序的调试等。
多多使用快捷键,提升效率,

—些调试的实例。

在这里插入图片描述
死循环
在这里插入图片描述
main()函数里面的都是局部变量,局部变量存储在栈区中,栈是从高地址向低地址存储,数组是从低地址向高地址存储,随着数组索引的增加,如果数组合适的向后越界,就有可能遇到i,就有可能把i改变,从而导致死循环。

如何写出好(易于调试)的代码。

编程常见的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值