C语言易混易忘易错警告

1.短路求值问题

a++和++a的区别

  • a++返回++之前的值再进行+1
  • ++a先进行++再返回++之后的值

或(||)非(~)与(&&)运算顺序

  • && 左边返回真值才会执行后边的
  • || 前边返回值非真才会执行后边的

2.指针数组、数组指针

指针数组:

  • 用来存指针的数组,比较简单与普通数组定义方式没有什么不同

数组指针:

  • 本质是指针,一个数组的指针
  • 这里先来说一下指针是如何定义的:要指向的类型 + * + 变量名
  • 怎样去判断一个变量的类型:去掉变量名,剩下的是类型
  • 怎样判断一个指针指向的类型:去掉*,去掉变量名,剩下的就是类型
  • 例如:定义一个数组:int arr[5] = {0};去掉变量名得到变量类型int [5],即一个int类型的数组,再去定义一个指针,int *p[5],这里存在一个问题,由于运算顺序的原因p先会去跟[5]结合,再跟 * 结合,这样还是一个指针数组,所以进行调整变成int (*p)[5] 就变成了一个数组指针。

3.二维数组

二维数组的实质

  • 一个二维数组int arr[2][3] 他的真正含义是:一个数组,里面包含两个元素,其中,每个元素又是一个包含三个元素的数组。
  • 如果将这个二维数组当作一维数组来看的话,那么第一个[] 代表元素的个数,后边的[]与前面的int 类型共同构成元素的类型,做这样解释“含两个元素的数组,每个元素都是一个长度为五的 int 数组”。

二维数组的定义

  • 参照一维数组定义时可以不给定数组个数,那么二位数组再定义时的第一个[] 中可以不给值,但是第二个[] 中不许要有值

二维数组的初始化

初始化 得到的结果
完全初始化:
arr[2][3] = {1,2,3,4,5,6};
{{1,2,3},{4,5,6}}
完全初始化:
arr[2][3] = {{1,2,3},{4,5,6}};
{{1,2,3},{4,5,6}}
不完全初始化:
arr[2][3] = {1,2}
{{1,2,0},{0,0,0}}
不完全初始化:
arr[2][3] = {{1},{2,3}}
{{1,0,0},{2,3,0}}

[]*的作用

结果
[] 偏移 + 间接引用
* 间接引用

arr和&arr的区别

结果
arr 得到数组首元素的地址
&arr 得到整个数组的地址
这个在偏移与间接引用中需要注意

4.typedef 、 define、const

typedef:

  • typedef:给已有的类型取一个别名
  • typedef'+ 类型 + 别名 + ;

宏(#define):

  • 宏(define):单纯的替换
  • #define + 宏的名字 + 要替换的内容

typedef 和 #define 的区别:
例如对于一个语句:

AA p1,p2;

操作 结果
typedef int* AA; int* p1;int* p2;
//变量p1,p2都是int 类型*

define AA int| int p1,p2;
// 变量p1是int类型,p2是int类型

有参数的宏:

  • 例如用宏去实现两个数相加:

define ADD(a,b) a+b

//括号里面的参数用来替换后边的 a,b

需要注意的是,”宏只是简单的替换“,所以在使用的时候还是要注意运算顺序

const:

  • 修饰一个常量,被修饰的值不可被修改
  • const + 类型 + 变量 + = + 初始值 + ; // const位置可以改变
  • 用const修饰的变量必须在定义的时候初始化
  • 实质上const并不是真正的不可被修改,通过指针的方法还是可以对其进行修改的。实质上它是修饰了一个只读。
操作 结果
int const *p = &a; p 的值可以更改
*p 的值可以更改
int * const p = &a p 的值不可以更改
*p 的值可以更改
int const * const p = &a; p 和 *p的值都不可以更改

5.位运算

& 与:

  • 都是1为1

13 & 6 = 4

执行的操作是0000 1101& 0000 0110 = 0000 0100结果是4

| 或:

  • 有1就为1

~ 取反:

  • 0变1 1变0

^ 异或:

  • 相同为0 不同为1
  • 使用异或可以实现不需要第三变量而交换两个变量的值,操作如下:
a = a ^ b;
b = a ^ b;
a = a ^ b;

<< 左移:

  • 左移补0

char a = 1;
a << 2;

执行的操作是0000 0001左移变成0000 0001 00,移动之后后边用0补位

  • 实际上左移是一个乘2的过程,移动一位,乘一个2,移动两位乘两个2

>> 右移:

  • 右移补符号位

位运算的应用:

  • 如果某个事物有两种状态,那么可以用一个位上是0或是1来表示他的状态。比如一盏灯的开和关只有两种状态,那么一个char型的变量可以用来存储八盏灯的状态:
  • 给某一位赋值使用或(|)运算来实现:

char a = 0;
a = a | 4;
a = a & (255 - 4);

以上通过给a = a|4将a对应的二进制数的第3位由0变为1
通过a = a & 251将a对应的二进制数的第3位由1变为0

  • 检查某一位的状态可以使用与(&)运算来实现:

char a = 4;
if(a & 4)

以上判断结果为真则第3位为1,结果为假则第3位为0

6.函数

函数的声明:

  • 返回值类型 + 函数名 + ( 参数类型 ) + ;
  • 无返回值时使用void
  • 函数名不能跟系统函数重名
  • 参数不限,参数与参数之间用,隔开,数量不限

函数的实现

  • 返回值(类型) + 函数名 + 参数 + { + 函数具体内容 + }
  • 函数的实现必须跟其声明保持一致

函数的调用

  • 函数名 + ( 具体的参数 ) + ;

函数传参

  • 函数在调用的时候参数有一个值传递的过程,函数定义里的参数为形参,在调用时的参数的参数才是实参,调用时实际上是用实参的值给形参去赋值。
  • 不想修改实参的时候,使用值传递
  • 想要修改实参的时候,就使用地址传递

函数指针

  • 函数调用的本质是:函数的地址 + 参数
  • 函数的名字就是一个函数指针,指针指向该函数的地址
  • 如何定义一个函数指针:类型 + * + 变量名,例如:int (*p)(int,int); 需要注意的时候*需要先跟变量名结合,之后再跟()结合
  • 实际上函数也可以这样来调用:(&pFun)(10);只是我们平时不这样去写
  • 利用typedef对 “函数指针” 类型重命名,typedef (*pFun) (int,int);
  • 函数的参数也可以传函数指针类型,用来通过传入函数地址去调用其他函数
  • 参见: C++函数名与函数指针.

7.堆区栈区

  • 一个C语言程序运行时,系统要为其分配相应的内存空间,
内存空间 功能
堆区 存放手动申请的空间
栈区 存放局部变量
全局/静态区 存放全局/静态变量
该区分为数据段和BSS块
未初始化的变量存在BSS,初始化过的变量存在数据段
字符常量区 存放字符串和常量
打的所有代码
  • 手动申请空间
    1.使用void* malloc(size_t size);手动在堆区申请一块空间
    (1)申请空间并返回这块空间的首地址,返回一个泛型指针,返回什么类型取决于使用者定义的变量类型,例如:int * p = (int * )malloc(4);
    (2)手动申请的空间不会随着作用域的结束而回收;
    (3)使用范围较广,在整个程序中只要能拿到它的首地址,就能使用这块空间。
    2.使用void free(void* memblock);对手动申请的空间进行回收
    (1) 一块空间只能释放一次;
    (2)释放必须是首字节地址;
    (3)free之后记得给指针赋值为NULL
    (4)如果不free,手动申请的空间在程序结束时才会被回收;

  • 局部变量:
    1.生命周期:声明时存在,作用域结束时消亡
    2.使用范围:只在所声明的作用域内有效

  • 堆区空间和栈区空间相比有什么不同:
    1.栈区的空间在程序运行起来之后就固定了,不能操作不属于自己的内存(栈溢出)
    2.堆区的空间在程序运行的时候,可以不断的malloc和free
    3.malloc申请出来的空间是连续的,也就是说可以使用指针偏移,可以申请一块数组

  • 全局变量
    1.在作用域之外定义的变量
    2.生命周期:程序运行时出生,程序结束时消亡
    3.使用范围:整个程序,
    4.在同一个项目中,全局变量跨文件使用的时候要在其前面加上extern

extern是计算机语言中的一个关键字,可置于变量或者函数前,以表示变量或者函数的定义在别的文件中。提示编译器遇到此变量或函数时,在其它模块中寻找其定义,另外,extern也可用来进行链接指定。
5.默认初始化为0

  • 静态变量:
    1.static修饰的变量,可以看作是特殊的全局变量
    2.生命周期:程序运行时出生,程序结束时消亡
    3.使用范围:在所声明作用域内
    4.默认初始化为0,只初始化一次

8.字符串以及字符串操作

  • 什么是字符串:
    字符串的实质是以\0结尾的字符数组
  • '\0''0'的区别:
    在ASCII码表中'\0' == 0 '0' == 48
    比如未完全初始化的char型数组也可以通过%s输出,就是因为其未完全初始化,后边自动以0补全,0对应的就是'\0'
  • 字符串的定义与初始化:
  1. 上面我们说字符串的本质就是一个以'\0'结尾的字符数组,那么我们就可以通过声明一个字符数组,将结尾初始化为'\0'就得到一个字符串:"char str[5] = {'a','s','d','f',0};
  2. char str1[] = "asdf"; //"asdf"存储于字符常量区 而str是在栈区声明的一块字符数组,这种定义与初始化方式是将字符常量区的字符串复制到字符数组中。
  3. ""双引号单独出现代表该字符串在字符常量区首元素的地址。那么我们可以在栈区定义一个char型指针去指向字符常量区字符串的首元素地址:char *str2 = "asdf";
  4. 以上三种方式之间有什么区别呢?
    (1) str毋庸置疑本质还是一个数组,按照数组的操作执行即可
    (2) 对于str1和str2,可以通过
    printf("%p\n",str1);
    printf("%p\n",str2);

    去查看它们所指向的地址空间,发现这两个所指向的空间不同,那是因为str1指向的是栈区,只是将字符常量区的字符串复制了过去。而str2直接指向字符常量区的字符串首地址。

strlen()函数

  • 功能:获取字符串长度
  • 用法:参数需要给定字符串首地址,返回字符串长度
    例如:int len = strlen("absd");
  • 注意:长度不包含\0,但是如果是一个字符数组的话求大小就要包含\0
// 手动实现strlen()的代码
int MyStrlen(char *str)
{
	int count = 0;
	while(*str != '\0')
	{
		count++;
		str++;
	}

	return count;
}

strcpy()函数:

  • 功能:将一个字符串赋到另一个字符串或者字符数组里面覆盖原来字符串或字符数组内容
  • 用法:strcpy(str1,str2); //str1目标字符串,str2原字符串,这里将字符串str2拷贝到str1中
  • 注意:strcpy()这个函数不安全,可能会出现目标字符串空间不足存放原字符串的情况,从而出现栈溢出的错误。

在return 0之后就会弹出这样的错误提示:使用了不属于自己的内存
在这里插入图片描述
所以我们用strcpy_s(str1,size,str2)来替换strcpy(str1,str2),这样在调试过程中就会在走到strcpy_s()这一行就提示出现错误,如下:
在这里插入图片描述
这样就能及时发现错误并解决。

//手动实现strcpy()函数的代码
char *MyStrcpy(char *str1,char *str2)
{
	char *pMark = str1;
	while(*str2 != '\0')
	{
		*str1 = *str2;
		str1++;
		str2++;
	}
	*str1 = '\0';

	return pMark;
}
  • strncpy(str1,str2,n); //截取str2中前n个字符拷贝到str1中
    当然这个函数也是不安全的,要使用安全的就是strncpy_s(str1,size,str2,n);
//手动实现strncpy()功能代码
char *MyStrncpy(char *str1,char *str2,int n)
{
	int i;
	for(i=0;i<n;i++)
	{
		str1[i] = str2[i];
	}

	return str1;
}

strcat()函数:

  • 功能:字符串拼接函数
  • 用法:strcat(str1,str2); //将str2拼接到str1后边并存到str1中,返回str1字符串的首地址
  • 注意:这个函数同上面一样要注意安全问题,所以我们通常使用strcat_s(str1,size,str2);函数。
//手动实现strcat()函数代码
char *MyStrcat(char *str1,char *str2)
{
	char *pMark = str1;
	//1.先找到目标'\0'位置
	while(*str1 != '\0')
	{
		str1++;
	}

	//2.遍历源字符串 把每个元素放到目标里
	while(*str2 != '\0')
	{
		*str1 = *str2;
		str1++;
		str2++;
	}
	*str1 = '\0';

	return pMark;
}
  • 同样这个函数也有strncat(str1,str2,n);函数与strncat_s(str1,size,str2,n);函数将字符串str2前n个字符拼接到字符串str1后边。

strcmp()函数:

  • 功能:将两个字符串从左到右逐个按照ASCII码表中对应的值进行比较
  • 用法:strcmp(str1,str2); //str1 = str2,返回0 str1 < str2,返回负数 str1 > str2,返回正数
  • 注意:同上存在strncmp(str1,str2,n);用来比较str2前n个字符与str1是否相同。

getchar()函数:

  • 功能:从输入缓冲区读取一个字符
  • 用法: char c = getchar();
  • 注意:返回值是int 类型,由于各国输入语言不同,所以它返回的是一个编码值,在英文中就是ASCII码值。

9.链表

定义一个链表
一个链表由两部分构成:他们是链表的内容和指向下一个链表的指针。所以我们可以这样来定义一个最简单的结构体。

typedef struct NODE  //type ... List在这里只是给这个结构体取一个别名,真正的链表部分是struct NODE{}部分
{
	int id;  
	struct NODE* pNext;
}List;

如果我们用刚才的链表去创建一个变量并且去初始化它,我们可以这样做:

//第一种方法
List a = {250,NULL};
//第二种方法
List b;
b.size = 520;
b.pNext = NULL;
//第三种方法
List p* = (List*)malloc(sizeof(List));
p->size = 502;
p->pNext = NULL;

我们可以看到,上面的的三个变量,a,b,p,他们都是相互独立的,相当于一个个独立的结构体,我们要形成链表就要把他们串起来,那么可以进行以下操作:

a.pNext = &b;
b.pNext = p; //p本身就是一个结构体指针,所以这里不用&

经过上面操作就形成了一个以a为头,以p为尾的链表。

链表遍历
链表实质上是一个结构体,只是在每个结构体里面都包含一个用于寻找下一个结构体的指针,通过这个指针我们就能将很多个这样的结构体(我们称之为结点)穿成一个链式结构

这样我们只要知道头结构体的地址,就可以通过它,一步步顺藤摸瓜找到后面的一系列结构体了

所以我们可以这样来遍历链表

List *p  = &a;
while(p->pNext != NULL)
{
	//要执行的操作
	p = p->PNext;
}

链表添加
链表添加的思想大概可以分为以下几步:
1.判断链表中是否有节点,即判断头节点是否指向NULL
2.如果链表中没有结点,那么就让头指针指向要添加的节点,再让尾指针指向这个节点
3.如果链表中有节点(头节点部位NULL),那么就先让尾指针指向的节点的pNext指针指向要添添加的节点,再让尾指针指向该节点

下面提供一段示例代码

//为了便于操作链表,通常会定义两个指针,一个指向链表头,一个指向链表尾
List* pHead = NULL;   //这里的List是链表结构体名
List* PEnd = NULL;
 void AddList(List** pHead,List** pEnd,List* pTemp)   //**pHead是头指针的地址,**pEnd是尾指针的地址,*pTemp是要添加的节点
 42 {  
 43     if(NULL == *pHead)
 44     {
 45         //链表中没有节点
 46         *pHead = pTemp;
 47     }
 48     else
 49     {
 50         //链表中有节点
 51         (*pEnd)->pNext = pTemp;   //这里注意运算符优先级,要给*pEnd加括号,否则它会和后面的->先运算
 52     }
 53     *pEnd = pTemp;
 54     
 55 } 

链表插入
链表插入的话又可以分为三种情况,但是不管哪种情况操作单向链表都需要遵循先连后断的原则,否则就会出现问题:
1.头部插入:新来的节点指向头指针,头指针指向新来的节点
2.中间部分插入:定义一个指针pMark用来遍历链表寻找你要插入的位置的前一个位置并标记,新来的节点指向的pNext指向pMark指针后面一个节点,pMark的pNext指向新来的节点
3.尾部插入:尾节点的pNext指向新来的节点,尾节点指向新来的节点

下面是示例代码:

List* pMark = NULL;
void InsertNode(List **ppHead,List **ppEnd,List *pNode,int id)  
{
	List *pMark = *ppHead;
	//1.头插入
	if((*ppHead)->id == id)
	{
		//新来的节点的下一个指向头
		pNode->pNext = *ppHead;
		//头指向新来的
		*ppHead = pNode;
		return;
	}

	//2.中间插入
	//遍历链表找到要插入位置的前一个节点
	while(pMark->pNext != NULL)
	{
		if(pMark->pNext->id == id)
		{
			//新来的下一个指向标记的下一个
			pNode->pNext = pMark->pNext;
			//标记的下一个指向新来的节点
			pMark->pNext = pNode;
			return;
		}
		pMark = pMark->pNext;
	}

	//3.尾部添加
	(*ppEnd)->pNext = pNode;
	*ppEnd = pNode;
}

链表删除
链表的删除应该也分为三种情况:
这里我们用到一个删除标记pDel用来指向你要删除的节点,以便删除
1.删除链表头:删除标记指向头节点,头指针指向头指针的下一个,删除释放删除标记指向的节点
2.删除中间节点:pMark遍历链表找到要删除的节点的前一个位置,pDel标记到要删除的节点,pMark的pNext指向pDel的后面一个节点,删除并释放pDel所指向的节点
3.删除链表尾:利用上面的删除中间节点的代码是可以做到删除尾节点的,但是存在一个问题,就是尾节点没有更新,删除尾节点之后,pEnd指向的就是一块不属于自己的空间了,所以在上面操作的基础上,我们判断一下要删除的是不是尾节点,如果是尾节点,我们就需要更新尾节点了

下面是一段示例代码:

List *pMark = NULL;
List *pDel = NULL;
void DeleteNode(List **ppHead,List **ppEnd,int id)
{
	List *pDel = NULL;
	List *pMark = *ppHead;
	//1.头删除
	if((*ppHead)->id == id)
	{
		//删除标记指向头节点
		pDel = *ppHead;
		//头指针指向头指针的下一个
		*ppHead = (*ppHead)->pNext;
		//释放删除标记
		free(pDel);
		pDel = NULL;
		return;
	}

	//2.中间和尾删除
	//遍历链表 找到要删除的节点的前一个节点
	while(pMark->pNext != NULL)
	{
		if(pMark->pNext->id == id)
		{
			//删除标记指向遍历标记的下一个
			pDel = pMark->pNext;
			//遍历标记的下一个指向遍历标记的下一个的下一个
			pMark->pNext = pMark->pNext->pNext;
			//释放删除标记
			free(pDel);
			pDel = NULL;
			//判断删除的是或否是尾节点
			if(pMark->pNext == NULL)
			{
				*ppEnd = pMark;
			}
			return;
		}
		pMark = pMark->pNext;
	}
}

双向链表
在上面的操作中,我们要对某一个特定的节点操作时,都是让pMark指针停在这个节点的前一个节点上,这是因为,如果我们让pMark直接指向这个节点的话,那么接下来你只能操作这个节点以及它之后的节点,而不能操作它之前的节点,我们将这样的链表称为单向链表,它只能单向寻址,也就是说只能从头到尾遍历,而不能从尾到头遍历

那我我们在在单向链表的基础上给它再添加一个指针pLast,让它实现从尾到头遍历的功能就能得到一个双向链表了,我们来看一段代码:

typedef struct NODE
{
	int id;
	struct NODE *pLast;
	struct NODE *pNext;
}List;

上面这个结构体组成的链表就可以实现双向遍历了。

10. 随机数

C语言中提供一种产生随机数的方法,在使用随机数之前要执行一次这个函数srand((unsigned int) num),我们叫做埋随机数种子,它的参数是一个unsigned int 类型的,我们可以随便给,在使用rand()函数就可以得到一个随机数,实际上他并不是真正的随机,实际上是根据你给的那个数进行一系列复杂的计算(具体不去探究)得到一个伪随机数返给你

你会发现你的每一个rand()函数之间的数是不同的,但是同一条rand()函数不同时间调用得到的结果是相同的

那么就是说我们埋种子的时候unsigned int参数可以给它一个一直变化的值,我们就可以得到一个真正的随机数了,我们用time(time_t*)这个函数,time()函数在文档中的说明是,它的返回值是从1970年1月1日0点0分0秒一直到现在经历的秒数,它的参数是给一个64位的int类型的指针,它最终计算得到的秒数除了通过返回值返回之外也会保存在这个指针所指向的地址空间里面。这样的话,我们只要每一条rand()语句执行的时间间隔在一秒之外,就可以获得一个真正的随机数了

srand((unsigned int)time(NULL));  //埋一个随机数的种子
printf("%d\n",rand());

11.栈(stack)

栈的规则是先进后出,弹夹就是一种很形象的栈

12.队列(queue)

队列的规则是先进后出,类似于我们现实生活中排队,只不过这里我们不允许插队

GetString()函数:

char *GetString()
{
	int size = 5;
	char *str = (char*)malloc(size);
	char c;
	char *newstr = NULL;
	int count = 0;
	char *pMark = str;	//永远指向老字符串的首地址

	//循环从输入缓冲区中取一个字符
	while((c = getchar()) != '\n')
	{
		//把取下的字符放到申请的空间里
		*str = c;
		//str先后移动
		str++;
		//计数加加
		count++;
		//判断是否能放下
		if(count + 1 == size)
		{
			//放不下
			//让str变成字符串
			*str = '\0';
			//size变大
			size += 5;
			//申请新的空间
			newstr = (char*)malloc(size);
			//字符串拷贝
			strcpy_s(newstr,size,pMark);
			//释放老的空间
			free(pMark);
			//str指向新字符串'\0'位置
			str = newstr + count;
			pMark = newstr;
		}
	}
	*str = '\0';

	return pMark;
}

冒泡排序法

	int arr1[10] = {9,0,1,2,3,4,5,6,7,8};
	int i,j;
	int flag = 1;

	for(j=0;j<10-1;j++)
	{
		flag = 1;
		for(i=0;i<10-1-j;i++)
		{
			if(arr1[i] > arr1[i+1])
			{
				arr1[i] = arr1[i] ^ arr1[i+1];
				arr1[i+1] = arr1[i] ^ arr1[i+1];
				arr1[i] = arr1[i] ^ arr1[i+1];
				flag = 0;
			}
		}
		if(1 == flag)
		{
			break;
		}
	}

二分查找

int BinaryFind(int arr[],int len,int n)
{
	int begin = 0;
	int end = len - 1;
	int mid = (begin + end) / 2;

	while(begin <= end)
	{
		mid = (begin + end) / 2;
		if(arr[mid] < n)
		{
			begin = mid + 1;
		}
		else if(arr[mid] > n)
		{
			end = mid - 1;
		}
		else
		{
			return mid;
		}
	}

	return -1;
}
posted @ 2023-03-15 14:14  Free152  阅读(27)  评论(0)    收藏  举报