uninhibited

导航

 

初识C语言(简单了解)

第一个C语言代码

#include <stdio.h>            /*头文件,std:standard标准,i:input输入;O:output输出*/
int main()                    /*主函数---程序的入口。一个程序有且仅有一个main函数。*/
{                             /*main前面的int表示main函数调用返回一个整型值*/
	printf("hello word!\n");  /*print:打印 f:function函数;库函数---C语言本身提供给我们使用的函数*/
	return 0;
}

数据类型

常用类型

数据类型 含义
char 字符数据类型(向内存申请一块空间放内容;例:char ch=‘A’;使用%c输出字符格式的数据)
short 短整型(short int)
int 整型(%d打印整型十进制数据)
long 长整型(long int)
long long 更长的整型
float 单精度浮点数(输出两位数的浮点数:printf("%.2f",a))
double 双精度浮点数

常用打印符号

% 含义
%d 打印整型
%c 打印字符
%f 打印浮点数(小数)
%p 以地址的形式打印
%x 打印16进制数字
%s 打印字符串
%u 打印无符号整型

数据类型的字节大小

#include <stdio.h>
int main()                              /*打印各个数据类型的字节大小*/
{
	printf("%d\n",sizeof(char));        
	printf("%d\n", sizeof(short));
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof(long));
	printf("%d\n", sizeof(long long));
	printf("%d\n", sizeof(float));
	printf("%d\n", sizeof(double));
	return 0;
}

输出结果(以字节位单位)image-20211207200042340

C语言标准规定:sizeof(long)>=sizeof(int)即可,以上输出结果中 sizeof(long)=sizeof(int)是满足条件的,有些编译器中的sizeof(long)输出结果为8位。

计算机中的单位

单位 含义
bit 比特位(只能存放一个1或者0的单位)
byte 字节(1byte=8bit)
kb 千字节(1kb=1024byte)
mb 兆字节(1mb=1024kb)
........ ………

进制转换

转成十进制

  • 二进制转十进制:把二进制数按权展开、相加即得十进制数。例:1011=1×20+1×21+0×22+1×23=11
  • 八进制转十进制:把八进制数按权展开、相加即得十进制数。例:7566=6×80+6×81+5×82+7×83=3958
  • 十六进制转十进制:把十六进制数按权展开、相加即得十进制数。例:FA03=3×160+0×161+10×162+15×163=64003

十进制转成其他

方法:十进制数除2取余法,即十进制数除2,余数为权位上的数,得到的商值继续除,直到商为0为止。

image-20211207204603366

常量与变量

概念

  • 常量:生活中有些值是不变的(比如:圆周率,性别,身份证号码,血型等)
  • 变量:有些值为可变的(比如:身高,体重,年龄等)

变量

定义变量的方法
int main()
{
	short age = 20;           /*向内存申请2个字节(16bit位),用来存放20*/
	float weight = 95.6f;     /*向内存申请4个字节,用来存放小数,后面的f表示该数字为浮点型*/
	return 0;
}
变量的分类
int num = 20;         /*全局变量---定义在代码块({})之外的变量*/
int main()
{
	int num2 = 10;    /*局部变量---定义在代码块内部的变量,只能在该代码块内使用*/
	return 0;
}

注:局部变量和全局变量的名字建议不要相同,容易误会,产生bug。当局部变量和全局变量的名字相同时,局部变量优先。

scanf函数(输入函数)
#include <stdio.h>
int main()
{
	int num1 = 0;
	int num2 = 0;
	scanf_s("%d%d", &num1, &num2);/*&:表示取地址符号,即在内存中找到该地址,把数据放进去*/
	printf("%d%d\n", num1, num2);
	int sum = num1 + num2;
	printf("%d\n", sum);
}

输出结果:

无标题

注:scanf_s 是VS编译器定义的,若要在 VS 使用标准的scanf函数需在第一行加上 “#define _CRT_SECURE_NO_WARNINGS 1”,这样就不会报错了(很多C语言自带的传统函数都是不安全的,如scanf、strcpy、strlen、strcat….等,加上前面的代码就不会产生报错),若不想每次都手动加上这行代码,可以在安装路径中找到 newc++file.cpp 这个文件用记事本打开,直接在里面加入代码即可,若是记事本修改后无法保存,可以使用 notepad++这个软件进行修改

安装路径:

变量的作用域和生命周期

作用域(scope):程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

  • 局部变量的作用域为变量所在的局部范围
  • 全局变量的作用域为整个工程

生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。

  • 局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束
  • 全局变量的生命周期:整个程序的生命周期

常量

常量的分类
  • 字面常量:直接写出来的值,如 int num=4;

  • const 修饰的常变量:

    #include <stdio.h>
    int main()
    {
    	const int num = 4;    /*const表示常属性,使用const定义后的变量不能再改动。num本质上还是变量,只不过具备了常属性(一个中国人拿到了美国绿卡,本质上还是中国人)*/
    	printf("%d\n", num);
    }
    
  • #define定义的标识符常量:

    #define MAX 10 /*定义MAX为常量,且其值为10*/
    
  • 枚举常量:关键字 enum

    #include <stdio.h>
    enum Sex
    {
    	MALE,
    	FEMALE,
    	SECRET
    };                 /*MALE,FAMALE,SECRET为枚举常量,它们有自己的值如下*/
    int main()
    {
    	enum Sex s = FAMALE;
    	printf("%d\n", MALE);    //0
    	printf("%d\n", FEMALE);  //1
    	printf("%d\n", SECRET);  //2
        printf("%d\n", s);
    	return 0;
    }
    

注:定义的枚举常量 (MALE ,FEMALE和 SECRET)不能改,但是后面定义的 s是可以改变的

字符串、转义字符和注释

  • 字符串:由“ ” 引起来的一串字符称为字符串字面值,或者简称字符串

注:字符串的结束标志是一个 \0 的转义字符。在计算字符串长度的时候 \0 是结束标志,不算作字符串内容。

#include <stdio.h>
int main()
{
	char arr1[] = "abc";               /*"abc"-----'a' 'b' 'c' '\0'; \0为字符串的结束标志;'\0'的输出结果为0*/
	char arr2[] = { 'a','b','c' };     /*所以在此句后面加上0或者'\0',两条语句输出就一样了。打印完三个字符后没遇到结束标志,就随机打印*/
	printf("%s\n", arr1),
	printf("%s\n", arr2);
	return 0;
}
#include <stdio.h>
#include <stdlen.h>
int main()
{
	char arr1[] = "abc";            /*只计算abc三个*/
 	char arr2[] = { 'a','b','c' };  /*一直统计随机值知道遇到'\0'(或者遇到’0’的值0)才停止,输出结果中的’15’为随机数*/
 	printf("%d\n", strlen(arr1));   /*strlen---string lenght---计算字符串长度*/
	printf("%d\n", strlen(arr2));
	return 0;
}

输出结果

  • 转义字符:即把原来的意思改变,如 ‘n’ 原始意思就为字符 ‘n’ ,但是 ‘\n’ 的意思为换行。若要打印 ‘\’ 需在其前面再加上一个 \ 。

转义字符表

转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\v 垂直制表(VT) 011
\\ 代表一个反斜线字符''\' 092
\' 代表一个单引号(撇号)字符 039
\" 代表一个双引号字符 034
? 代表一个问号 063
\0 空字符(NUL) 000
\ddd(ddd表示1~3个八进制数) 1到3位八进制数所代表的任意字符 三位八进制
\xhh(dd表示2个十六进制数字) 十六进制所代表的任意字符 十六进制
#include <stdio.h>
#include <stdlen.h>
int main()
{
	printf("%d\n", strlen("C:\test\32\test.c"));                                /*输出结果应为13,对应的字符有'C',':','\t','e','s','t','\32','\t','e','s','t','.','c';其中'\32'表示为32是2个8进制数字,即32作为8进制代表的十进制数字作为ASCII码值所对应的字符(32--->10进制为26--->作为ASCII码值代表的字符为→)*/
	printf("%c\n", '\32');
	return 0;
}

输出结果:

ASCII表

  • 注释 :(/**/或者//)
    • 代码中有不需要的代码可以直接删除,也可以注释掉
    • 代码中有些代码比较难懂,可以加一下注释文字

注:/**/这种注释方法不能嵌套,前一个/*只能跟最近的*/相匹配

原码、反码和补码

机器数和真值

  • 机器数:一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。

    那么,这里的 00000011 和 10000011 就是机器数。

  • 真值:因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

原码,反码和补码

  1. 原码:符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。如:

    [+1]=0000 0001

    [-1]=1000 0001

  2. 反码:正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反。如:

    [+1] = [0000 0001]=[0000 0001]

    [-1]=[1000 0001]=[1111 1110]

  3. 补码:正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)

    [+1]=[0000 0001]=[0000 0001]=[0000 0001]

    [-1]=[1000 0001]=[1111 1110]=[1111 1111]

    补码--->原码:取反+1(跟-1再取反是一样的)

常见关键字

auto(自动,局部变量也称为自动变量,出了作用域会自动销毁);break;case;char;const;continue;default;do;double;else;enum;extern;float;for goto;if;int;long;register(寄存器);return;short;signed(有符号);sizeof;static(修饰函数);struct(结构体关键字);switch;tyedef(类型定义,类型重重命名);union(联合体,共用体);unsigned(无符号);void(无);volatile;while

typedef使用示例:

#include <stdio.h>}
int main()
{
	typedef unsigned int u_int;
	unsigned int num = 20;
	u_int num2 = 20;
	return 0;    //使用typedef重新定义里unsigned int的类型为u_int,此段程序中的u_int和unsigned int表示一样的数据类型
}

static使用示例:

#include <stdio.h>}
void test()
{
	static int a = 1;                 //static修饰局部变量,使其生命周期变长,使下一次进入test函数时a不会重新定义为1,ststic也能修饰全局变量,此时改变的是变量的作用域,让静态的全局变量只能在自己所在的源文件内部使用,不能被调用到源文件外位置;static也能用来修饰函数,作用结果与全局变量一样,改变了函数的链接属性,正常的函数具有外部链接属性,使用时声明(extern)下就能使用,使用static修饰后使函数变为内部链接属性
	a++;
	printf("a=%d\n", a);
}
int main()
{
	int i = 0;
	while (i < 5)
	{
		test();
		i++;
	}
	return 0;        //输出结果为a=2,3,4,5,6
}

#define定义常量和宏

  • #define定义标识符常量

    #define MAX 1000

  • #define定义宏

示例:

#include <stdio.h>}
int Max(int x, int y)
{
	if (x > y)
		return x;
	else
		return y;
}
# define MAX(X,Y) (X>Y?X:Y) 
int main()
{
	int a = 10;
	int b = 20;
	int max1 = 0;
	int max2 = 0;
	max1 = Max(a, b);    //函数的方式进行比较
	max2 = MAX(a, b);    //宏的方式进行比较,在语句运行时,会直接变成语句(a>b?a:b);
	printf("%d\n", max1);
	printf("%d\n", max2);
	return 0;              //两个printf打印的结果都是20
}

基本语句

选择语句

  • 使用 if_else 语句实现选择两个选项中的选择
#include <stdio.h>
int main()
{
	int select=0;
	printf("你考上了大学\n");
	printf("你是否要好好学习?(1/0)\n");
	scanf("%d", &select);
	if (select == 1)
		printf("你会称为大佬!");
	else
	printf("准备回家继承百万家产把!");
	return 0;
}

循环语句

  • 使用 While 语句实现循环效果
#include <stdio.h>
int main()
{
	int line = 0;
	printf("进入大学\n");
	printf("开始写代码\n");
	line = 1;
	while (line < =500)
	{
		printf("第%d行代码\n", line);
		line++;
	}
	if (line > 500);
	printf("学业有成");
	return 0;
}

函数

函数的基本使用

示例:一个简单的加法函数

#include <stdio.h>
int Add(int x, int y)   //返回值z为整型,所以前面记得加上int
{
	int z = x + y;
	return z;
}
int main()
{
	int sum = 0;
	int c = 0;
	int d = 0;
	scanf("%d%d\n", &c, &d);
	sum = Add(c, d);
	printf("%d\n", sum);
	return 0;
}  

数组

数组的使用

#include <stdio.h>
int main()
{
	int i = 0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };  //定义一个存放10个整数数字的数组
	while (i < 10)
	{
		printf("%d\n", arr[i]);
		i++;
	}	
	if (i >= 10)
	{
		printf("输出完毕\n");
	}
	return 0;
}

注:存放进数组的元素都有一个下标,下标从0开始,如果是n个元素,那第n个元素对应的下标为n-1,我们可以通过下标来访问元素

操作符

操作符
算术操作符 +、-、*、/、%(取余)
移位操作符(二进制位) >>(右移)、<<(左移)
位操作符(二进制位) &(按位与)、^(按位异或)、|(按位或)
赋值操作符(复合赋值) =、+=、-=、*=、/=、&=、^=、|=、>>=、<<=、 (a=a+10==a+=10)

与:同时为真才为真;或:一个为真即为真;异或:对应的二进制为相同则为假,相异则为真

单目操作符(只有一个操作数) 含义
! 逻辑反操作(变成假或真,假--0,真----非0)
负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
对一个数的二进制按位取反(0->1;1->0)c
−− 前置,后置--
++ 前置,后置++
* 间接访问操作符(解引用操作符)
(类型) 强制转换类型
  • --和++的使用(-- 跟++同理)
#include <stdio.h>
int main()
{
	int a = 10;
	int b = a++;              //后置++,先使用a赋值给b,在++(即a=a+1) 
	printf("%d %d\n", a, b);
	return 0;
}
#include <stdio.h>
int main()
{
	int a = 10;
	int b = ++a;                 //前置++,先++,再赋值给b
	printf("%d %d\n", a, b);
	return 0;
}
关系操作符 含义
> 大于
< 小于
>= 大于等于
>= 小于等于
!= 不等于
== 相等
逻辑操作符 含义(只关系真假)
&& 逻辑与
|| 逻辑或
条件操作符(exp代表表达式)–三目操作符 含义
exp1?exp2:exp3; 如果表达式1成立,则输出表达式2,否则输出表达式3
int main()
{
	int a = 10;
    //int a = 100;
	int b = 20;
	int max = 0;
	max=a > b ? a : b;
	printf("%d\n", max);
	return 0;                   //输出结果位b的值(20),若注释掉第三行,用第四行的a,则输出位a的值(100)
}

其他操作符

操作符 含义
[] 下标引用操作符
() 函数调用操作符
. 结构体指向对象(结构体.元素)
-> 结构体指针指向对象(结构体指针—>元素)

指针

基础使用

整型指针示例:

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = 0;             //*表示后面的p为指针变量,int表示其类型
	p=&a;                   //取a在内存中的地址赋值给p
	                        //指针变量——用于存放地址,用(int*)来定义
	*p=20;                  //*--解引用操作符/间接访问操作符,找到p所指向的对象(a)再赋值为20
	printf("%p\n", &a);
	printf("%p\n", p);
	printf("%d\n",a);
	return 0;
}

使用其他类型的指针时,把上面示例中的类型定义改成所需类型即可,如下char示例

#include <stdio.h>
int main()
{
	char ch = 'a';
	char* p = &ch;
	*p = 'b';
	printf("%p\n", &ch);
	printf("%p\n", p);
	printf("%c\n", ch);
	return 0;
}

指针变量的大小

32位的 计算机上指针大小为32bit,即4个byte;64位计算机上指针大小为64bit,即8个byte(主要是看存储地址的数据大小)。

#include <stdio.h>
int main()
{
	char ch = 'a';
	char* p = &ch;
	printf("%d\n", sizeof(p));
	//*p = 'b';
	//printf("%p\n", &ch);
	//printf("%p\n", p);
	//printf("%c\n", ch);
	return 0;         //输出结果为4,若是将配置管理器调为64位,则输出结果为8
}

image-20211212152114959

结构体

使用示例

#include <stdio.h>
#include <string.h>
struct Book                      //使用struct定义结构体,在里面加入所需要的属性
{
	char name[20];
	short price;
};
int main()
{
	struct Book b1 = { "C语言程序设计",25 };   //给上面定义好的结构体的属性进行输入
	struct Book*p = &b1;                      //将b1的地址放在指针变量*p中
	printf("%s\n", (*p).name);                //通过指针的方式输出其名字和价格
	printf("%d\n", (*p).price);
    printf("%s\n", p->name);              //也可以使用这种方式输出上面一样的结果
    printf("%d\n", p->price);
    printf("%s\n", b1.name);           //使用(名字.属性去调用属性或者对属性内容进行编辑)
	printf("%d\n", b1.price);
	b1.price = 15;                   //b1.name不能直接用这种赋值的方式改,因为它是个数组,而不是变量,如果要改需用函数strcpy库函数,即字符串拷贝直接把要改后的名字拷贝到b1.name中
    //strcpy(b1.name,"C++");
	printf("%d\n", b1.price);
	return 0;
}

“.” ——>结构体变量 . 成员

“->” ——>结构体指针->成员

分支和循环语句

分支语句

  • if
  • switch

循环语句

  • while
  • for
  • do while

goto语句

分支语句(选择结构)

if语句

语法结构

  • 单分支
if(表达式)
    语句;
    
  • 双分支
if(表达式)
    语句1;
else
     语句2;
  • 多分支
if(表达式1)
    语句1;
else if(表达式2)      //该语句需要特别注意,例:else if(18<=aeg<=28)这样的判断方式是不对的,因为会先判断18<=age,若此时给定的值为10,则会先算出来为0,然后在跟28进行比较,显然0<28,所以还是会输出次else后面的结果,正确的语句因为(age>=18 && age<=28)
    语句2;
else if(表达式3)
    语句3;
.....

如果条件成立要执行多个代码语句,需用{}括起来

#include <stdio.h>
int main()
{
	int age = 0;
	printf("请输入你的年龄:");
	scanf("%d", &age);
	if (age < 18)
	{
		printf("你是个青年\n");
		printf("还不能谈恋爱!\n");
	}
	else
	{ 
		printf("你是个大人了\n");
		printf("可以为自己做主\n");
	}
	return 0;
}

注:else与其最近的且没被匹配的 if 所对应

if书写形式的对比

if(condition)
{
    return x;
}
return y;
if(condition)
{
    return x;
}
else
{
    return y;
}

以上两代码表示的意思一样 (都为条件成立执行语句x,条件不成立执行语句y) ,但是第二段代码的书写形式更好,逻辑意思清晰

#include <stdio.h>
int main()
{
	int a = 2;
	if (a == 1)
	{
		printf("相等\n");
	}
	else {
		printf("不相等\n");
	}
	return 0;
}
#include <stdio.h>
int main()
{
	int a = 2;
	if (1 == a)
	{
		printf("相等\n");
	}
	else {
		printf("不相等\n");
	}
	return 0;
}

以上两段代码意思也相同,但是第一段代码的(==)容易写成(=)且编译器不会提示跟报错;但是第二段代码写错了会有提示,尽量使用第二种形式

练习:1. 判断一个数是否为奇数

  2. 输出1-100之间的所有奇数
#include <stdio.h>
int main()
{
	int a = 1;
	while(a<=100)
	{ 
		if (a % 2 != 0)
		{
			printf("%d\n", a);
		}
		a++;	
	}
	return 0;
}

switch语句

switch语句也是一种分支语句。常常用于多分支的情况。

#include <stdio.h>
int main()
{
	int day = 0;
	scanf("%d", & day);
	switch (day)
	{
		case 1:
			printf("星期一\n");
			break;
		case 2:
			printf("星期二\n");
			break;
		case 3:
			printf("星期三\n");
			break;
		case 4:
			printf("星期四\n");
			break;
		case 5:
			printf("星期五\n");
			break;
		case 6:
			printf("星期六\n");
			break;
		case 7:
			printf("星期天\n");
			break;
		default:
			printf("请输入正确的天数");
	}
	return 0;
}

switch语句的基本使用方法如上代码所示,case后面带有break,不然会直接执行在此情况后面的情况,导致输出有错误,若是不属于case的情况外的使用default来概括,如果有多种情况的执行的语句一样时,只需要一个执行语句跟break即可。

注:case 后面须为整型常量表达式,不能使用变量或者整型之外的其他数据类型

循环语句

while循环

语法结构

while(表达式)
    循环语句;

image-20211214150145514

输入函数解释

类似scanf,getchar等输入函数都是将输入的值放在输入缓存区中,如果输入缓存区中没有值的话光标就会停住等待输入值,但是如果本来就有值的话会直接去走里面的值,如以下示例:输入密码后按回车,前面的代码部分就会被scanf取走,但是留下来的回车,也就是“\n”并没有被取走,所有后面程序执行到getchar时会直接取走“\n”,所以并不会停留,而是直接执行下面的语句并输出。

#include <stdio.h>
int main()
{
	int password = 0;
	int ret = 0;
	printf("请输入密码:>");
	scanf("%s", &password);
	printf("确认你的密码(Y/N):>");
	ret = getchar();
	if (ret == 'Y')
	{
		printf("确认成功\n");
	}
	else
		printf("确认失败\n");
	return 0;
}

image-20211214164926090

输出结果:image-20211214165023302

改进:

#include <stdio.h>
int main()
{
	char password[20] = { 0 };
	int ret = 0;
	int ch = 0;
	printf("请输入密码:>");
	scanf("%s", &password);
	printf("确认你的密码(Y/N):>");
	while ((ch = getchar()) != '\n');      //用循环来确认第一个getchar函数把输入缓冲区的数据全部取走,这样才能使后面一个getchar在缓存区里面没有值可以取,它才会等待 输入
	{
		;
	}
	ret = getchar();
	if (ret == 'Y')
	{
		printf("确认成功\n");
	}
	else{
		printf("确认失败\n");
	}
	return 0;
}

for循环

语法结构

for (表达式1;表达式2;表达式3)
    循环语句;

表达式1为初始化部分,用于初始化循环变量的;表达式2为条件判断部分,用于判断循环什么时候终止;表达式3为调整部分,用于循环条件的调整。

例:使用for循环打印1~10的数字

#include <stdio.h>
int main()
{
	int a = 0;
	for (a = 1; a <= 10; a++)
		printf("%d\n", a);
	return 0;
}

image-20211215163303598

for循环的一些建议:

  • 不可在for循环体内修改循环变量,防止for循环失去控制。
  • 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。
//前闭后开区间
#include <stdio.h>
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int a=0;
int main ()
{
    for (a=0;a<10;a++)
    {
        printf("%d\n",arr[a]);
    }
    return 0;
}
//前闭后闭区间
#include <stdio.h>
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int a=0;
int main ()
{
    for (a=0;a<=9;a++)
    {
        printf("%d\n",arr[a]);
    }
    return 0;
}

注:1. for循环的初始化、调整、判断都可以省略,但是 for 循环的判断部分如果被省略,那判断条件就是:恒为真。
2. 如果不是非常熟练,建议不要随便省略相关的代码。

do…while()循环

语句语法

do
{
    循环语句;
}
while(表达式);

image-20211215183620785

函数

函数的分类

  • 库函数:C语言本身提供给我们的函数称为库函数(经常要使用的)
  • 自定义函数:

库函数

常用的库函数有:

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

C语言函数网站

库函数使用示例:

  • strcpy使用

    #include <stdio.h>
    #include <string.h>			//调用含有要使用函数的库函数
    int main()
    {
    	char arr1[]= { "liang" };
    	char arr2[20] = { "****************" };
    	strcpy(arr2,arr1);
    	printf(" %s\n", arr2);			//打印结果为“liang”strcpy函数把"/0"也复制过去了,打印函数遇到该字符会自动停住打印,故后面的***...没有打印出来
    	return 0;
    }
    
  • memset使用

    #include <stdio.h>
    #include <memory.h>
    int main()
    {
    	char arr[] = ("Hello World");
    	int a = '&';
    	memset(arr, a, 5);
    	printf("%s\n", arr);
    	return 0;
    }
    

自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的空间。

函数的组成:

ret_type fun_name(paral,*)
{
    statement;				//语句项
}
ret_type  返回类型
fun_name  函数名
paral     函数参数

函数的参数

  • 实际参数:真实传给函数的参数叫做实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传给形参。
  • 形式参数:形式参数指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完之后就自动销毁了。因此形式参数只在函数中有效。

函数的调用

  • 传值调用:函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。

  • 传址调用:

    • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
    • 这种传参方式可以让函数和函数外边的变量建立其真正的联系,也就是函数内部可以直接操作函数外部的变量。

    需要对函数外部的参数进行操作就使用传址调用,不用进行操作就使用传值调用

函数的嵌套调用和链式访问

函数和函数之间可以有机的组合。

  • 嵌套调用:在函数在嵌套使用另一个函数。

  • 链式访问:把函数的返回值作为另一个函数的参数。

    #include<stdio.h>
    int main()
    {
    	int len = 0;
    	len = strlen("abc");
    	printf("%d\n", len);
    	printf("%d\n", strlen("abc"));
    	return 0;
    }
    

函数的声明和定义

  • 函数声明:

    • 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在没有关系。
    • 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
    • 函数的声明一般放在头文件之中。
    #include<stdio.h>
    //#include"add.h"    引用自己定义的头文件使用""。
    int ADD(int, int);   //函数声明,有些编译器不声明的话,如果函数放在后面的话可能会报警告:ADD未定义。
    int main()
    {
    	int a = 1;
    	int b = 6;
    	int sum = 0;
    	sum = ADD(a, b);
    	printf("%d", sum);
    	return 0;
    }
    
    int ADD(int x, int y)
    {
    	int z = 0;
    	z = x + y;
    	return z;
    }
    
    #ifndef __ADD_H__
    #define __ADD_H__
    //函数的声明
    int Add(int x,int y);
    #endif
    //头文件这样定义能有效防止同一个头文件被引用多次而造成代码量过多
    
  • 函数定义:函数的定义是指函数的具体实现,交代函数的功能实现。

函数递归

  • 什么是递归:程序调用自身的编程技巧称为递归( recursion)。递归作为一种算法程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
  • 递归的两个条件:
    • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
    • 每次递归调用之后越来越接近这个限制条件。

数组

一维数组的创建和初始化

数组的创建

数组是一组相同类型元素的集合。

数组的创建方式:

type_t arr_name [const_n];
// type_t 是指数组的元素类型
// const_n 是一个常量表达式,用来指定数组的大小,注意只能是常量,不能是变量

数组的初始化

数组的初始化是指在创建数组的同时给数组的内容一些合理初始值(初始化)。

int arr[10]={1,2,3}    //不完全初始化,数组的前三位才有给定的值,剩下的元素默认为0
int arr2[3]={1,2,3}    //完全初始化,数组里面的元素的值都为自己给定的
char arr3[]="abcd"     //数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定

数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定

一维数组的使用

数组的使用时用到操作符 : [] ,下标引用操作符。它其实就是数组访问的操作符

  • 数组是使用下标来访问的,下标是从0开始。

  • 数组的大小可以通过计算得到。

    int arr[10];
    int sz=sizeof(arr)/sizeof(arr[0]);
    

一维数组在内存中的存储

数组在内存中是连续存放的

image-20220124164415825

二维数组的创建与初始化

二维数组的创建

int arr[3][4];      //三行四列
char arr[3][5];		//三行五列
double arr[2][2];	//两行两列

第一行代码表示:

image-20220124164827203

二维数组的初始化

int arr[3][4]={1,2,3,4,5}		//不完全初始化,没定义的值为0
int arr[3][4]={{1,2,3},{4,5}} //用一维数组的初始化来初始化二维数组,第一行会放入1,2,3,0;第二行会放入4,5,0,0;第三行则全是0.
int arr[][3]={{1,2,3},{1,2}}	//二维数组的列不能省略,行可以省略。

image-20220124170135794

二维数组的使用

二维数组的使用也是通过下标的方式

#include<stdio.h>
int main()
{
	int i, j;
	int arr[][3] = { {1,2,3},{1,2} };
	int sz = sizeof(arr) / sizeof(arr[0][0]);	//计算元素个数
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", arr[i][j]);
		}	
			printf("\n"); 
	}
	return 0;
}

输出结果:image-20220124171750080

二维数组在内存中的存储

#include<stdio.h>
int main()
{
	int i, j;
	int arr[][3] = { {1,2,3},{1,2} };
	int sz = sizeof(arr) / sizeof(arr[0][0]);	//计算元素个数
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("&arr[%d][%d]=%p\n", i,j,&arr[i][j]);
		}	
	}
	return 0;
}

输出结果:image-20220124172201247 结论:二维数组在内存中也是连续存储的

image-20220124172539740

数组作为函数参数

在对数组进行传参时,实际上传过去的是数组arr首元素的地址&arr[0]。

使用示例(冒泡排序:将数组里的元素按照一定的顺序排列):

void Bubble_sort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	int temp = 0;
	for (j = 0; j < sz; j++)
		for (i = 0; i < sz - 1-i; i++)
		{ 
			if (arr[i] > arr[i + 1])
			{ 
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
}
#include<stdio.h>
int main()
 {
	int arr[] = { 3,4,2,6 };
	int a = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);   //因为数组传参传的是首元素的地址,所以要计算元素个数时先要在外面算好再一起传过去。
	Bubble_sort(arr,sz);
	for (a = 0; a < sz; a++)
		printf("%d ", arr[a]);
	return 0;
}

注:数组名通常情况下为首元素的地址,除以下两种情况:

  • sizeof(数组名),计算整个数组的大小。sizeof 内部单独放一个数组名,数组名表示整个数组,单位为字节;
  • &数组名.,取出的是数组的地址。&数组名,数组名表示整个数组。

操作符

操作符的分类

  • 算术操作符
  • 移位操作符
  • 位操作符
  • 赋值操作符
  • 单目操作符
  • 关系操作符
  • 逻辑操作符
  • 条件操作符
  • 逗号表达式
  • 下标引用、函数调用和结构成员

算术操作符

+ - * / %

  • 除了%操作符之外,其他的几个操作符可以作用于整数和浮点数。
  • 对于/操作符,如果两个操作数都为整数,执行整数除法。二只要有浮点数执行的就是浮点数除法。
  • %操作符的两个操作数必须均为整数。返回的是整除之后的余数。

移位操作符

  • << 左移操作符(左边丢弃,右边不0,相当于*2)

image-20220127223310994

  • >> 右移操作符(相当于/1,1和-1除外)
    • 算术右移:右边丢弃,左边补原符号位(基本见到的移位都为算术移位)
    • 逻辑右移:右边丢弃,左边补 0

(移动的是二进制位)注:他们的操作符必须为整数。注:他们的操作符必须为整数。

image-20220127223351920

位操作符

  • &:按位与(对应的二进制位有0为0,同时为1才是1)
  • |:按位或(对应的二进制位有1为1,同时为0才是0)
  • ^:按位异或(对应的二进制位相同为0,相异为1)

注:他们的操作符必须为整数。

int main()
{
	int a = 3, b = 5;
	int c = a & b;
	int d = a | b;
	int f = a ^ b;
	printf("%d %d %d", c, d, f);
}

与:image-20220127231229677

或:image-20220127231253443

异或:image-20220127231319406

结果:image-20220127231357485

赋值操作符

没啥好记得。。。

复合赋值符image-20220127234548189

单目操作符

只有一个操作数

image-20220127235004997

关系操作符

image-20220128004409647

逻辑操作符

关注其本身是真还是假,真为1(不为0的数),假为0

注:逻辑与只要左边为假,右边就不会再算了;逻辑或只要左边为真,也不会计算右边了

image-20220128004434998

条件操作符

exp1?exp2:exp3(表达式1为真,执行表达式2,表达式1为假,则执行表达式3)

逗号表达式

image-20220128010121824

下标引用、函数调用和结构成员

image-20220128011339053
结构体使用示例:

#include <stdio.h>
#include <string.h>
struct Book                      //使用struct定义结构体,在里面加入所需要的属性
{
	char name[20];
	short price;
};
int main()
{
	struct Book b1 = { "C语言程序设计",25 };   //给上面定义好的结构体的属性进行输入
	struct Book*p = &b1;                      //将b1的地址放在指针变量*p中
	printf("%s\n", (*p).name);                //通过指针的方式输出其名字和价格
	printf("%d\n", (*p).price);
    printf("%s\n", p->name);              //也可以使用这种方式输出上面一样的结果
    printf("%d\n", p->price);
    printf("%s\n", b1.name);           //使用(名字.属性去调用属性或者对属性内容进行编辑)
	printf("%d\n", b1.price);
	b1.price = 15;                   //b1.name不能直接用这种赋值的方式改,因为它是个数组,而不是变量,如果要改需用函数strcpy库函数,即字符串拷贝直接把要改后的名字拷贝到b1.name中
    //strcpy(b1.name,"C++");
	printf("%d\n", b1.price);
	return 0;
}

隐式类型转换

C 的基本类型中并非是完全的对立,部分数据类型之间是可以进行隐式转换的。隐式转换指的是不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道发生了哪些转换(对于无符号为的数据类型补0)。

image-20220128014212868

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。

image-20220128015126590

如果某个操作数的类型在上图中的排名较低,那么首先要转换为另一个操作数的类型后执行运算。

操作符的属性

  • 操作符的优先级

img

  • 操作符的结合性
  • 是否控制求值顺序(逻辑与和逻辑或会控制求值顺序,条件操作符和逗号表达式也需要)

image-20220128020312086

image-20220128020649805

初级指针

指针是什么

在计算机中,所有的数据都是存放在存储器中的,不同的数据类型占有的内存空间的大小各不相同。内存是以字节为单位的连续编址空间,每一个字节单元对应着一个独一的编号,这个编号被称为内存单元的地址。比如:int 类型占 4 个字节,char 类型占 1 个字节等。系统在内存中,为变量分配存储空间的首个字节单元的地址,称之为该变量的地址。地址用来标识每一个存储单元,方便用户对存储单元中的数据进行正确的访问。在高级语言中地址形象地称为指针。

指针就是变量,用来存放地址的变量(存放在指针中的值都被当做地址来处理)

  • 在32位的机器上,地址是32个0或1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  • 在64位的机器上,那指针变量的大小是8个字节。

指针和指针类型

  • 指针类型决定了指针进行解引用时能访问的空间大小,如int*解引用时能访问4个字节,而char*解引用时只能访问1个字节。

  • 指针类型决定了指针的步长。

野指针

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

成因:

  • 指针未初始化

  • 指针越界访问

  • 指针指向的空间释放(定义的空间被销毁了)

    int *test()
    {
        int a=10;
        return &a;		//返回的确实是a的地址,但是在这个函数结束后a的空间就会销毁还给系统,所以后续再用刚刚的地址访问的就不再试a的值了
    }
    int main()
    {
        int *p=test();
        *p=20;
    }
    

如何避免野指针:

  • 指针初始化(实在不知道初始化成啥的的时候可以直接初始化成NULL)
  • 小心指针越界
  • 指针指向空间释放之后将其置为NULL
  • 指针使用之前检查有效性

指针运算

指针+-整数

指针-指针

指针和数组

二级指针

指针也是一个变量,既然是变量那么也会有存放的内存地址,指针存放的内存地址就是二级指针

指针数组

存放指针的数组

int* arr[3]={&a,&b,&c}
*(arr[i])			//解引用数组中的指针

结构体

结构体的声明

struct tag		//struct--结构体关键字,tage--结构体名称/标签
{
    member-list;
}variable-list
    //例如描述一个学生(名字、年龄、ID)
struct student
{
    char name[20];
    short age;
    int ID;
}si,s2,s3;			//是三个全局的结构体变量(尽量少使用)

typedef struct Student		//使用typedef将struct student重新命名为下面的Stu,可以直接使用Stu来定义结构体变量
{
    char name[20];
    short age;
    int ID;
}Stu,		//在这里的Stu表示的是类型
 
int main()
{
    struct student a={"张三",12,454343636};	//创建结构体变量(局部变量),没创建变量之前的结构体定义不占空间,创建结构体变量才会占用空间(这里不重新命名的话一定要使用struct)
    Stu 
}

结构体的初始化

在创建结构体时给其赋值(如果该结构体里面包含另一个及结构体使用{},{}里面也需要进行初始化)

struct student a={"张三",12,454343636};

结构体的访问

结构体的传参

传值和传址都可以,使用传址比较好(不会重新创建另一个结构体)

调试

test.c 可执行程序
Debug Debug版本的可执行程序(可以进行调试,因为文件中包含了调试信息,故文件会比较大)
Release Release版本的可执行程序(进行了各种优化,使的程序在代码大小和运行速度上都是最优的,以便用户的使用)

常用调试快捷键

快捷键 用法
F5 启动调试,经常用来直接调到下一个断点处
F9 创建断点和取消断点
F10 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
F11 逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部
CTRL+F5 开始执行不调试,如果你想程序直接运行而不是调试时可以直接使用

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

优秀的代码

  • 代码运行正常
  • bug很少
  • 效率高
  • 可读性高
  • 可维护性高
  • 注释清晰
  • 文档齐全

常见的coding技巧

  • 使用assert(assert()里面为假,则会报错,里面条件为真,会继续执行下面的程序)
  • 尽量使用const(const放在指针变量的*左边,修饰的是*p,也就是说:不能改变*p的值,const修饰哪个变量哪个就不能改。)
  • 养成良好的编码风格
  • 添加必要的注释
  • 避免编码的陷阱

编程的常见错误

  • 编译型错误:直接看错误提示信息(双击),解决问题,相对来说比较简单
  • 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识名不存在或者拼写错误
  • 运行时错误:借助调试,逐步定位问题。最难搞。

数据的存储

数据类型详细介绍

内置类型:

类型 含义
char 字符数据类型
short 短整型
int 整型
long 长整型
long long 更长的整型
float 单精度浮点数
double 双精度浮点数

类型的意义:

  • 使用这个类型开辟内存空间的大小(大小决定了使用范围)
  • 如何看待内存空间的视角

类型的基本归类

  • 整型家族

    • char
      • unsigned char(无符号位,最前面的二进制位不看成符号位,能存放的数值比有符号位的数值要大)
      • signed char
    • short
      • unsigned short[int]
      • signed char short[int]
    • int
      • unsigned int
      • signed char int
    • long
      • unsigned long[int]
      • signed long[int]
  • 浮点型家族

    • float:单精度浮点型
    • double:双精度浮点型
  • 构造类型

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

    • int *pi;
    • char *pc;
    • float *pf;
    • void *pv;
  • 空类型:void表示空类型(无类型),通常用于函数的返回类型、函数的参数、指针类型。

    有符号char图示:

    image-20220130175248283

整形在内存中的存储:原码、反码、补码

原码、反码和补码

机器数和真值

  • 机器数:一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。

    那么,这里的 00000011 和 10000011 就是机器数。

  • 真值:因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

原码,反码和补码

  1. 原码:符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。如:

    [+1]=0000 0001

    [-1]=1000 0001

  2. 反码:正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反。如:

    [+1] = [0000 0001]=[0000 0001]

    [-1]=[1000 0001]=[1111 1110]

  3. 补码:正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)

    [+1]=[0000 0001]=[0000 0001]=[0000 0001]

    [-1]=[1000 0001]=[1111 1110]=[1111 1111]

    补码--->原码:-1再取反

注:无符号数的原码、反码和补码都是相同的

大小端字节介绍及判断

大小端介绍

  • 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位则保存在内存的低地址中;
  • 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位则保存在内存的高地址中;

为什么会有大小端:

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个"如何将多个字节安排"的问题。因此就导致了大端存储模式和小端存储模式。
 例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

大小端判定:

#include<stdio.h>
check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	return *p;
}
int main()
{
	int ret=check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

浮点型在内衬中的存储解析

常见的浮点数:

3.14159 1E10 浮点数家族包括:float、double、long double类型。浮点数的范围:float.h中定义

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

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

image-20220130235949320

image-20220131000224774

image-20220131000317780

image-20220131000456216

image-20220131001940778

#include<stdio.h>
int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("* pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("* pFloat的值为:%f\n", *pFloat);

	return 0;
}

指针详解

字符指针

数组指针

image-20220201221651909

指针数组

数组传参和指针传参

一维数组传参

image-20220201225155895

二维数组

函数指针

函数指针数组

指向函数指针数组的指针

回调函数

void*

image-20220205161318810

字符函数和字符串函数

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。字符串常量适用于那些对它不做修改的字符串函数。

求字符串长度

strlen

语法形式:size_t strlen (const char*str); size_t在此函数内被重新命名了,它其实就是unsigned int。

  • 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中'\0' 前面出现的字符个数(不包含'\0')。
  • 参数指向的字符串必须要以'\0'结束。
  • 注意函数的返回值为size_t,是无符号的。
  • 学会strlen函数的模拟实现。

长度不受限制的字符串函数(只以"\0"为停止点)

  • strcpy

    语法形式:char*strcpy (char*destination,const char*source);

    • 源字符串必须以'\0'结束。
    • 会将源字符串中的'\0' 拷贝到目标空间。
    • 目标空间必须足够大,以确保能存放源字符串。
    • 目标空间必须可变。
    • 学会模拟实现。
  • strcat(字符串追加)

    语法形式:char*strcat (char*destination,const char*source);

    • 源字符串必须以'\0' 结束。
    • 目标空间必须足够大,能容纳下源字符串的内容。
    • 目标空间必须能修改。
    • 不能自己给自己追加,会导致死循环。
  • strcmp(一对一对字符比较其ASCII值的大小)

    语法形式:int strcmp (const char* str1,const char* str2);

    • 比较标准规定:
      • 第一个字符串大于第二个字符串,则返回大于0的数字。
      • 第一个字符串等于第二个字符串,则返回0。
      • 第一个字符串小于第二个字符串,则返回小于0的数字。

长度受限制的字符串函数

  • strncpy

    语法形式:char* strncpy (char*destination,const *source,size_t num);

    • 拷贝num个字符从源字符串到目标空间。
    • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,知道num个。
  • strncat

    语法形式:char*strcat (char*destination,const char*source,size_t num);

  • strncmp

    语法形式:int strcmp (const char* str1,const char* str2,size_t num);

    • 比较到出现另一个字符不一样或者一个字符串结束或者num个字符全部比较完。

      image-20220206223244641

字符串查找

  • strstr(查找子字符串)

    语法形式:char*strstr(const char*string,const char*strCharSet);

  • strtok、

    语法形式:char*strtok (char*str,const char*sep);

    • sep参数是个字符串,定义了用嘴分隔符的字符集合。
    • 第一个参数指定一个字符串,它包含了0个或者多个有sep字符串中一个或者多个分隔符分割的标记。
    • strtok函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
    • strtok函数的第一个参数不为NULL,函数将找到str中的第一个标记,strtok函数将 保存它在字符串中的位置。
    • strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
    • 如果字符串中不存在更多的标记,则返回NULL指针。

错误信息报告

  • strerror(返回错误码所对应的错误信息。)

    语法形式:cha*strerror (int errnum(这里直接打errno就行,这里的错误信息代码是库函数维护的));

    • 须包含头文件<errno.h>(errno是一个全局的错误码的变量,当C语言的库函数在执行过程中,发生了错误,就会把对应的错误码,赋值到errno中)

字符操作

字符分类函数:

函数 如果它的参数复合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格' ',换页'\f',换行'\n',回车'\r',制表符'\t'或者垂直制表符'\v'
isdigit 十进制数字0~9
isxdigit 十六进制数值,包括所有十进制数字,小写字母a~f,大写字母A~F
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母a~z或字母A~Z
isalnum 字母或者数字,a~z,A~Z,0~9
ispunct 标点符号,任何不属于数字或者字母的图形符号(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符

字符转换:

  • int tolower(int c);
  • int toupper(int c);

内存操作函数

  • memcpy

    语法形式:void * memcpy(void * destination, const void* source, size_t num);

    • 函数memcpy从source的位置开始向后复制num个字节的数据到destination
  • memmove(处理内存重叠的情况)

    语法形式:void * memcpy(void * destination, const void* source, size_t num);

  • memcmp

    语法形式:int memcmp (const void*ptr1, const void*ptr2, size_t num);

    • 比较从ptr1和ptr2指针开始的num个字节

    • 返回值如下:

      image-20220207163113194

  • memset

    语法形式:void*memset(void*dest, int c, size_t count);

自定义类型

结构体

结构是一些值得集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

结构的声明

struct tag
{
    member-list;
}variable-list;				//若在此创建结构体变量的话,创建出来的变量是全局变量

int main()
{
    struct tag a1={,,};				//创建给构体变量a1,此处创建的结构体变量为局部变量。
}

结构体的自引用

错误的引用方式:

struct Node
{
    int date;
    struct Node next;	//这样引用的话无法计算分配的内存空间,因为是一直叠加下去的,会报错。
};

正确的引用方式;

struct Node
{
    int date;
    struct Node* next;	//在结构体中放入下一个结构体变量的指针才是正确的自引用方式。
};

结构体内存对齐

  • 结构体对齐规则:

    • 第一个成员(结构体里的元素)在与结构体变量偏移量为0的地址处。
    • 其他成员(结构体里的元素)变量要对齐到某个数字(对齐数)的整数倍的地址处。
    • 对齐数=编译器默认的一个对齐数与该成员大小的较小值(VS默认的对齐数为8,Linux中gcc没有默认对齐数,该成员大小即为对齐数)
    • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    • 如果嵌套了结构体的情况,嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数,不是结构体的大小)的整数倍。
  • 为什么存在内存对齐?

        1、平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

        2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需求一次访问。

    总体来说:

          结构体的内存对齐是拿空间来换取时间的做法。(对于数据能一次性的拿去完整)

    因此,设计存放的时候也是需要一定的技巧:应该让占用空间下的成员尽量集中在一起

  • 修改默认对齐数

    #pragma pack(8)		//设置默认对齐数为8
    #pragma pack()		//取消设置的默认对齐数,还原为默认
    #pragma pack(1)		//设置默认对齐数为1,直接一直往下放
    
  • offsetof(计算结构体成员与结构体变量的相对偏移量)

    语法:size_t offsetof(structName,memberName);

  • 结构体的传参(若要修改结构体里面的内容需要传址,不修改的话可以使用传值的传参方式)

    image-20220208000310411

位段

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

    • 位段的成员必须是int、unsigned int或signed int。

    • 位段的成员名后边有一个冒号和一个数字

      struct A
      {
          int _a:2;		//a只需要2个bit位就足够
          int _b:5;		//b只需要5个bit位就足够
      }
      
  • 位段的内存分配

    • 位段的成员可以是int、unsigned int、signed int或者是char(属于整型家族)类型。

    • 位段的空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的。

    • 位段涉及很多不确定因素,位段是不跨平台的,注意可移植的程序有应该避免使用位段。

      image-20220208004719878

  • 位段的跨平台问题

    • int位段被当成有符号数还是无符号数是不确定的。
    • 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
    • 位段中的成员在内存中从左向右分配,还是右向左分配标准尚未定义。
    • 当一个结构包含两个位段,第二个位段成员比较大,无法容纳与第一个位段剩余的位时,时舍弃剩余的位还是利用,这是不确定的 。
  • 总结:跟结构相比,位段可以达到相同的效果,但是可以很好的节省空间,但是有跨平台的问题。

枚举

  • 枚举类型的定义

    enum Day
    {
        Mon,
        Tues,
        Wed,
        Thur,
        Fri,
        Sat,
        Sun
    };
    
  • 枚举的优点:

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

联合(共用体)

  • 联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用一块空间

  • 联合体的定义:

    union Un
    {
        char c;
        int i;
    };
    
  • 联合大小的计算

    • 联合额大小至少是最大成员的大小。
    • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

动态内存分配

为什么存在动态内存分配

image-20220208134024699

之前所学习的通过创建变量和数组的方式来分配内存,所分配的内存大小是固定不变的,且在申请数组的内存空间时,必须指定数组的长度,若是在原先不知道所需的内存空间的大小的情况下,这种数组开辟空间的方式就不能满足了。

动态内存函数的介绍

malloc

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针

  • 语法形式:void*malloc(size_t size);

    #include<stdio.h>
    #include<string.h>
    #include<malloc.h>
    int main()
    {
        int *p=(int*)malloc(10*sizeof(int));		//向内存申请10个整型大小的空间,并强制类型转换成整型指针,且赋值给p
        if(p==NULL)
        {
            printf("%s",strerror(errno));
        }
        else
        {
            int i=0;
            for(i=0;i<10;i++)
            	*(p+i)=i; 
            for(i=0;i<10;i++)
                printf("%d ",*(p+i));
        }
        free(p);		//释放掉分配好的内存空间
        p=NULL;			//将p置为空指针	
        return 0;
    }
    
  • 如果开辟成功,则返回一个指向开辟的空间的指针。

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要检查。

  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候由使用者自己决定。

  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

free

C语言提供用来做动态内存的释放和回收的。

  • 语法形式:void free(void*ptr)

  • malloc和free成对使用,一个malloc必须有一个free。

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。

  • 如果参数ptr是NULL指针,则函数什么事都不做。

calloc

C语言还提供了一个函数叫calloc,calloc函数也用来动态内存分配

  • 语法形式:void*calloc(size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
  • 与函数malloc的区别只在于calloc会返回地址之前把申请的空间的每个字节初始化为0。
  • 开辟失败的话也会返回NULL。

realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间太大了,为例合理的分配空间,我们会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
  • 语法形式:void*realloc(void*ptr, size_t size);
  • ptr是要调整的内存地址。
  • size是调整之后的新大小。
  • 返回值为调整之后的内存起始位置
  • 这个函数调整原内存大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc使用注意事项:
    • 如果原空间之后有足够的内存空间可以追加,则直接追加,放回原空间的地址。
    • 若原空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域开辟一块满足需求的空间,并且把原来的数据拷贝过去,将旧内存释放,最后返回新内存的地址。
    • 得用一个新的变量来接收新的地址。

常见的动态内存错误

  • 对NULL的解引用操作
  • 对动态开辟空间的越界访问
  • 对非动态空间使用free
  • 使用free释放一块动态开辟内存的一部分
  • 对同一块动态内存多次释放
  • 动态开辟的空间忘记释放(导致内存泄漏)

内存区域分配

image-20220208172051626

C/C++程序内存分配的几个区域:

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
  • 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

柔性数组

C99中,结构中的最后一个元素允许时未知大小的数组,该数组的大小是可以调整的,这就叫做【柔性数组】成员

  • 柔性数组的特点:
    • 结构中的柔性数组成员前面至少一个其他成员。
    • sizeof返回的这种结构大小不包括柔性数组的内存。
    • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
    • 使用柔性数组比自己写的指针指向malloc开辟空间的方式更方便内存释放,也有利于访问速度(连续的内存有益于提高访问速度,也有益于减少内存碎片)。

文件操作

文件类型

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。

image-20220208233732907

文件缓冲区

ANSIC标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

image-20220208232624040

文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。改结构体类型是有系统声明的,取名FILE。

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

FILE* pf		文件指针变量,通过文件指针变量能够找到与它相关联的文件

文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针与文件之间的关系。

ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。

#include<stdio.h>
#include<errno.h>
int main()
{
	FILE*Pf=fopen("C:\\Users\\Dell\\Desktop\\test.txt", "w");		//打开文件
	if (Pf == NULL)
	{
		printf("%s\n", strerror(errno));		//判断文件是否打开成功
	}
	fclose(Pf);				//关闭文件
	Pf = NULL;				//将文件指针置为空指针
	return 0;
}

文件的打开方式:

文件使用方式 含义 如果指定文件不存在
"r"(只读) 为了输入数据,打开一个已经存在的文本文件 出错
"w"(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
"a"(追加) 向文本文件尾添加数据 出错
"rb"(只读) 为了输入数据,打开一个二进制文件 出错
"wb"(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
"ab"(追加) 向一个二进制文件尾添加数据 出错
"r+"(读写) 为了读和写,打开一个文本文件 出错
"w+"(读写) 为了读和写,建立一个新的文件 建立一个新的文件
"a+"(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
"rb+"(读写) 为了读和写打开一个二进制文件 出错
"wb+"(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
"ab+"(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

文件的顺序读写

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件
  • scanf/printf:是针对标准输入流/标准输出流的格式化输入/输出语句
  • fscanf/fprintf:是针对所有输入流/所有输出流的格式化输入/输出语句
  • sscanf/sprintf
    • sscanf是从字符串中读取格式化的数据
    • sprintf是把格式化数据输出成(存储到)字符串

文件的随机读写

  • fseek:根据文件指针的位置和偏移量来定位文件指针。

    语法形式:int fseek(FILE*stream, long int offset, int origin);

  • ftell:返回文件指针相对于起始位置的偏移量

    语法形式:long int ftell(FILE*stream);

  • rewind:让文件指针回到文件的起始位置

    语法形式:void rewind(FILE*stream);

文件结束的判定

feof:

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束。而是应用于当文件读取结束的时候,判断是因为读取失败而结束,还是遇到文件尾而结束。

  • 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
    • fgetc判断是否为EOF。
    • fgets判断返回值是否为NULL。
  • 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    • fread判断返回值是否小于实际要读的个数。

预处理

image-20220209152324010

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

#define

image-20220209174319545

#和##

  • 在C语言中宏定义中,#的功能是将其后面的宏参数进行字符串化操作。
  • ##是连接符,前加##或后加##,将标记作为一个合法的标识符的一部分,不是字符串.多用于多行的宏定义中,将位于它两边的符号合成一个符号。
  • image-20220209180448181

宏和函数

  • 宏的优势:
    • 拥有调用函数和从函数返回的代码可能比实际执行小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹(宏在预处理阶段就完成替换)。
    • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整型、长整型、浮点型等(宏是类型无关的)。
  • 宏的劣势:
    • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
    • 宏是无法调试的。
    • 宏由于类型无关,也就不够严谨。
    • 宏可能会带来运算符优先级的问题,导致程序容易出错。

区别:

属性 #define定义宏 函数
代码长度 每次使用是,宏代码都会被插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。 函数代码只出现于一个地方;每次使用这个函数是,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级 宏参数的求值实在所以周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 函数参数只在传参的时候求值一次,结果更容易控制
参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们执行的任务是相同的
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

#undef

这条指令用于移除一个宏定义。

条件编译

C语言条件编译详解

文件包含

include

  • 本地文件包含:#include "filename"

    查找策略:先在源文件所在目录下查找,如果该头文件为找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误

    • Linux路径:/usr/include
    • VS环境路径:C:\program File (x86)\Microsoft Visual Studio 9.0\VC\include
  • 库文件包含:#include<filename.h>

posted on 2022-04-05 15:53  CodeLiang  阅读(141)  评论(0)    收藏  举报