C语言的那些坑(随缘更新)

本文深入探讨C语言编程中的常见错误与高级技巧,包括函数参数管理、字符串处理、优先级理解、枚举类型使用、指针操作、数组声明、宏定义及回调函数实践,帮助程序员提升代码质量和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  • 对函数参数中的指针使用malloc容易犯的错误

    如果你需要在一个函数内对传入的指针使用malloc来分配内存,那么你需要传这个指针的指针,或者传这个指针的引用。请看下面的例子:

    void fun(char * data)
    {
    	data=(char *)malloc(10*sizeof(char));
    	...
    }
    int main()
    {
    	char * s;
    	fun(s);
    }
    

    在main函数中声明的s其实没有任何变化,因为data并不是s本身,传参的时候data是另一个指针然后指向了s所指的内容,本来是没有问题的,但是malloc是重新给data指针分配了地址,而并没有给外面的s重新分配地址,从此datas没有任何关系了。正确的做法是

    	void fun(char * &data)
    	{
    		data=(char *)malloc(10*sizeof(char));
    		...
    	}
    

  • 用scanf读取包含空格的字符串

    我们都知道scanf读取字符串时,遇到空白字符(空格或制表符)就会停止。
    而用以下方法可以读取包含空格的字符串,遇到换行符才停止:

     scanf( "%[^\n]", s);
    

    ^是表示补集的正则表达式,即除了\n全部匹配。


  • 优先级最低的运算符——逗号运算符

    表达式1 , 表达式2

    先计算左边的操作数,再计算右边的操作数。右操作数的类型和值作为整个表达式的结果。
    由于逗号运算符的优先级最低,因此下面的语句运行之后:

    		int a=(1,2,3);
    		int b=1,2,3;
    

    a的值是3,b的值是1
    括号改变了运算优先级导致先计算表达式1,2,3,因此a等于右操作数3


  • enum枚举类型的元素默认按0,1,2,… 赋初始值

    enum DAY
    	{
    		MON,TUE,WED,THU,FRI,SAT,SUN
    	};
    

    这样定义的枚举类型DAY中MON的值是0,TUE的值是1,以此类推
    如果:

    enum DAY
    	{
    		MON,TUE,WED=10,THU,FRI,SAT,SUN
    	};
    

    这样给某个元素赋初值,那么MON=0, TUE=1, WED=10, THU=11, FRI=12, SAT=13, SUN=14
    即,没有人为赋值的元素的值是上一个元素的值+1


  • char a = '好'; 错误的写法

    char类型的值占用1字节(Byte), 而一个汉字占用2字节,因此单个汉字不是字符而是字符串,要写: char a[]="好";这个字符数组占用3字节(一个汉字2字节,结束标识\0 1字节) 这个字符串的长度为2
    但是在UTF-8编码中,一个汉字占3字节
    GCC编译器默认使用UTF-8

     char a[]="好";
     printf("%d",strlen(a));
    

    输出:3

    而在UTF-16中一个汉字占4字节。除此之外的主流编码中,一个汉字都是2字节


  • func() 等价于 int func()

    定义函数没有指定函数返回值类型时,默认返回int值,因此下面这种写法是合法的:

    func()
    {
    	return 1;
    }
    

    但是会引发warning:
    在这里插入图片描述

    因此并不推荐这种写法


  • 向下取整是数值部分向下取整,与正负号无关

    这一点很容易理解,因为数值变量也是按二进制存储的,有专门的符号位,取整操作和符号位没啥关系,只是数值位上的截断

    int a= -1/3;
    

    a的值为0


  • 0 || -1的值是1

在逻辑表达式中,除了0是false(0),非0数值都是true(1)


  • &arr , arr , &arr[0] 的区别

int arr[10];

&arr , arr , &arr[0] 三者的值是完全相等的
可以认为 arr&arr[0] 是等价的,都是指数组首元素即arr[0]的地址,是int*类型的数据
而&arr是指整个数组的起始地址,虽然值和首元素的地址相等,但类型不同,&arr是int**类型的数据,即指向数组的指针类型,因此,当我们把三者分别加1时:

int arr[10];
printf("%d\n",arr);
printf("%d\n%d\n%d",&arr[0]+1,arr+1,&arr+1);

结果:

6421984
6421988
6421988
6422024

可以看出,&arr[0]+1arr+1表示的地址都只偏移了4个字节,而&arr+1表示的地址偏移了4*10=40个字节


  • #include 的两种写法的区别:

    #include <stdio.h>
    表示在系统的C头文件默认存储路径中寻找头文件stdio.h	
    
    #include "stdio.h"
     表示在当前目录(源文件所在路径)中寻找头文件stdio.h
    

    也就是说,第三方头文件得放在源码文件所在的目录下,并使用第二种方式引用


  • switch()里只能放整形变量或者字符变量(char),case后的值只能是常量const intconst char

  • switch case语句后面要加break

    如果没有加break,会从执行的那个case开始往下执行直到遇到break
int a=2;
    switch(a)
    {
        case 1:
            printf("1");
        case 2:
            printf("2");
        case 3:
            printf("3");
        case 4:
            printf("4");
            break;
        case 5:
            printf("5");

    }

输出:

234


  • 常量字符串的声明: const char * pchar *(const p):

    用const修饰的变量为常量,值不允许被改变,但如果用const修饰指针变量,则有上述两种方式,那么他们有什么区别呢?
    const char * p表示指针指向的地址内的值是常量指针的值(指向什么地址)可以改变,指针指向的地址内的值不可以改变。即我声明了一个const char类型的*p,这个p是一个const char类型值的储存地址。并且事实上这种声明的更清晰的写法是char const * p,这最直接地说明了*p是const类型
    比方说:我有如下定义:

    const char * p="abcd";  
    

    那么,

    p="efgh";
    

    是合法的,(PS:像 "efgh"这样的值属于const char *)因为’"abcd"这个值还在 那里,没有变,我只是新增了一个值"efgh",并让p指向它。
    但是

    *p="efgh";
    

    是非法的,对*p进行赋值就是在试图改变常量"abcd"

    char * const p;
    

    则是定义了一个char类型的*(const p) (去掉括号,结合方式不会变,这里只是为了看的更清楚一些),此时指针的值不可以改变,即指针不可以指向其他地址。但是指针指向的地址内的值是可以改变的
    但是要注意
    如此定义的指针不能指向"abcd"这样的常量字符串,即以下写法是错误的:

    *p="efgh";  
    

原因见下面的注意

注意!char *s="abcd";这种写法并不规范。因为"abcd"本身是属于常量类型const char *

经实验:在gcc中编译不会报错,能正常运行。但是一旦尝试更改"abcd"的值就会抛出异常
但是在VS2019中编译时就会出现这样的错误:
在这里插入图片描述
所以规范的写法是const char *s="abcd";具体含义见上文。
如果你非要这样写,为了顺利通过VS的编译,可以使用强制类型转换:

char *s = (char *)"abcd";

这样虽然不会报错,并且能被正常地读取,但s实际上仍是const char *类型,一旦你试图更改"abcd"这个常量字符串:
在这里插入图片描述
就会在运行时引发异常。因为字符串常量存储在常量存储区,该区域在运行时是只读的。
如果我们的需求就是在编译时给一个字符串赋以默认值,并且在运行时可能进行修改,应该怎么写?
请继续往下看:


  • 字符串&字符数组:

    在C语言中没有字符串的类型(C++中有string类),但是有字符串的概念,所谓字符串(character string)是以空字符'\0'结尾的char数组。
    scanf,gets等函数会自动在获取的字符串结尾加上'\0'
    而像这样:
    char s[]="aaa";
    char s[10] = "aaa";
    
    " "来给char数组赋值,编译器也会在数组的末尾加一个\0
    而用{ }来赋值就不会添加\0
    因此char s[]="a";数组长度是2char s[]={'a'}; 数组长度是1

  • 未经初始化的数组内容是内存中的不可预测数据

    int map[10][10];
    for(int i=1;i<=9;i++,putchar('\n'))
            for(int j=1;j<=9;j++)
     	       printf("%d ",map[i][j]);
    

    像这样,一个数组内的元素初始值并不是0,因为声明数组的过程只是指定了一块内存空间,而并未清除该段内存原本的数据,如果直接运行会出现下面这样的结果:
    rafe
    用下面的方法可以快速地初始化数组:

int arr[10][10]={0}; 

无论数组的维数是多少,这样都可以将数组内的元素全部初始化为0.
如果把0换成其他的数比如2,那么数组的第一个元素会是2,其余元素全部初始化为0

特别的,静态数组声明时会初始化为0

static int map[10][10];
for(int i=1;i<=9;i++,putchar('\n'))
        for(int j=1;j<=9;j++)
            printf("%d ",map[i][j]);

运行结果:

在这里插入图片描述


  • 巧用三元运算符

    条件表达式1表达式1 : 表达式2
    若条件表达式的值为true,则整句话的值是 表达式1的值
    若条件表达式的值为false, 则整句话的值是 表达式2的值

  • 宏定义函数

格式:

#deine func_name(parameter) expression

可以传入参数,返回expression的值
先来看一个最简单的例子:

	#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

a和b都用括号的原因是,宏定义函数中expression的计算只是单纯的把参数填进去,而这个参数的数据类型乃至格式是没有限制的。为了保证值的独立性,用括号包裹非常保险。
再来看一个实用的例子:

	#define MALLOC(n, type) ( (type *) malloc( (n) * sizeof(type) ) )

传入大小和类型名,在堆上开辟长度为n的该类型的空间并返回其指针(这里可以参考我写的另一篇C/C++程序运行的五种内存分区
普通的函数无法做到这一点,因为数据类型是不允许作为函数参数传递的,而宏函数因其直接替换的特性却可以做到。
如果表达式想换行,在除了最后一行的每行的末尾加上续行符\:

	#define MALLOC(n, type) \
	( (type *) malloc( (n) * sizeof(type) ) )

宏函数中还可以写return,会直接替换到调用处,下面给出一个最简示例:

#define f() \
	return 10
int func()
{
	f();
	return 1;
}

调用func函数,返回值是10而不是1,看到这里,相信你已经理解了宏定义函数的原理:简单粗暴的文本替换


  • 回调函数

先摘一段定义:

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

从知乎上看到的比较生动的解释(侵删):

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

C语言实现:

int callback(int a,int b)
{
	return a + b;
}
int dispose(int(*func)(int,int))
{
	int x = func(1,2);
	return 10 * x;
}
int main()
{
	printf("%d",dispose(callback));
}

输出:30

在上面的例子中,我们调用了dispose函数并向它传递了callback函数的地址,dispose函数接收该参数,并将传入的地址赋值给func函数指针,最后通过func成功调用callback函数。


  • 以数组作为形参的函数的声明方式

一维数组

 type func(int arr[10]); 
 type (int arr[]); 
 type (int *arr)

二维数组

type func(int **arr);
type func(int arr[][10]);  //**数组的第二维维度一定要显式指定**

  • Visual Studio出现_CRT_SECURE_NO_WARNINGS警告

方法1
右击项目—>属性–>转载自网络,侵删
方法2
在源文件首部添加:

#define _CRT_SECURE_NO_WARNINGS 

或者
右击项目—>属性–>C/C+±->预处理器 添加
_CRT_SECURE_NO_WARNINGS
在这里插入图片描述

  • 安全地快速初始化无穷大

memset(arr,0x3f,sizeof(arr));

使用0x3f填充数组,每项数值足够大的同时即使乘2也不会溢出,执行之后数组元素的值为:
在这里插入图片描述
若是要单项的无穷大数值,可以写

const int INF = 0x7fffffff;

不用数了,7个f,记住前面的数字就是后面 f 的个数即可
其数值为
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值