C语言编程实践中的易错点总结
持续更新中。。。
字符串相关易错点
-
1、给一个字符串创建动态内存来存储时,常见的错误用法
malloc(strlen(str))
,正确的用法应该是malloc(strlen(str)+1)
,出错原因是没考虑到strlen()函数只能统计字符串实际字符的个数,而忘了字符串结束符'\0'也要占用一个内存。 -
2、字符串和字符数组定义(即给变量分配内存,注意和“声明”的区别,“声明”不涉及内存,比如“声明函数原型”和“定义函数”)时,不能混淆,比如
char str[10];
语句只能作为字符数组但最好用char str[10] = {0};
来定义一个空字符数组。而char str[10] = "";
这个语句才应该是定义字符串的(其实和char str[10] = {0};
效果一样,都是给10个字节全初始化赋值'\0',即空字符)。区别就在于""其实是默认存入了一个'\0'字符串结束符,相当于一个字符串常量。如果将字符数组的定义方式用于定义空字符串,那么将会发生很多未知的内存错误。 -
3、在使用strncpy(str_dest, str_src, n) 函数,来将源字符串str_src的前n个字节复制并覆盖到目标字符串从str_dest地址开始的n个字节,后面地址若原有其他字符会继续保留。若目标字符数组是
char str_dest[20];
这样定义的,那么必须在使用完strncpy()函数后,加一行语句str_dest[n] = '\0'
,即手动添加字符串结束符。若目标字符数组是char str_dest[20] = "";
或char str[10] = {0};
这样定义的,就无影响。但安全起见可以习惯性添加(尤其是注意操作结构体中的字符串时,由于结构体内字符串的定义只能是char str_dest[20];
)但注意是原字符串的n个字符后面没有其他字符了,不然后面的字符串就无效了。 -
4、在使用strcpy(str_dest, str_src) 函数时,会将str_src地址开始到'\0'结束符的字符串(包括'\0'结束符)都复制并覆盖到str_dest开始的地址的相应字节数。原有的str_dest开始的字符串如果后面还有其他字符,不会被清空,但是将字符串str_dest打印出来会看不到后面的其他字符,是因为打印时先遇到str_src的'\0'字符就停止打印了,并不代表后面没有了字符。
-
5、上面3和4都要注意防止数组溢出。也就是存入的元素个数不能是大于数组定义的时候申请的内存大小。
-
6、要从一个函数中返回一个字符串
char * getString();
,在函数中定义这个字符串然后再返回,也是新手们容易犯的一个错误。因为函数内定义的变量都是自动变量(局部变量),函数调用结束后就会自动释放这个变量内存,返回的一个字符串首地址后面的内存其实已经被其他的内容覆盖而乱码了。解决方案有4中:
方法1:使用全局声明的字符数组变量,函数返回数组名(也就是字符串的首地址)。缺点是所有人都可以修改这个内存。
方法2:函数内使用静态字符数组,函数返回数组名(也就是字符串的首地址)。缺点是下一次调用的时候将覆盖掉这个数组内容,而且这块静态内存会一直存在,如果很大,不用的时候也没法主动释放,很浪费内存
方法3:函数内使用malloc()函数动态分配内存,函数返回内存指针(也就是字符串的首地址)。但缺点是调用者可能不知道函数内开辟了动态内存,而忘记用free()函数释放内存。
方法4:函数调用者在调用前主动分配动态内存,然后传入内存首地址做函数的形参,函数不需要再返回字符串地址。因为是调用者自己开辟的内存,所以很容易想起要free()释放内存。这种方法适合需要字符串的作用域范围大的场景,只要还没有free()释放内存,那么在其他的代码块或者函数内也都可以使用这个动态内存。
void getString(char * str, int size)
{
char str_temp[] = "";
。。。操作str_temp得到一个想要的字符串
strncpy(str, str_temp, size);
//上面四种方法在具体的字符串赋值时都应该使用strncpy()函数,而不能直接赋值。
}
调用函数:
char * buffer = malloc(sizeof(char)*size);
getString(buffer, size); //将字符串长度也传入,防止溢出错误
。。。
free(buffer); //容易记得用完释放内存。
方法5:类似于方法4,但是是用
char buffer[size];
代替char * buffer = malloc(sizeof(char)*size);
,使用局部变量内存来代替malloc开辟的堆内存,可以省掉free()的麻烦。使得这个buffer字符串的作用域限制在这个代码块或函数中,离开这个代码块或函数后会被自动释放。
指针使用错误
- 1、定义指针后,要先让指针指向一个明确的地址,然后才能使用指针(一般是用解除引用,若是字符串则不需要)。否则将一个空指针去引用内存或者作为参数赋给其他用到这个指针指向的地址值的函数,将引发段错误,导致程序中止。
- 2、复杂的带指针的声明的理解,比如:
char * const * (*do_sth)(int *p);
和char (* arr[10])[20];
理解分析的步骤:
1、首先,找到左边起的第一个普通变量名(非关键字)开始分析。如上面例子找到变量名do_sth
和arr[10]
。
2、根据变量名周围的优先级情况来进行下一个分析:
—— 2.1)先把变量名所在的被括号扩起的一部分先当做一个整体,如上例子(*do_sth)
和(* arr[10])
。先可以轻松读出do_sth是一个指向什么的指针,arr是一个数组且数组的每一个元素都是一个指向什么的指针。
——2.2)看整体的后缀符号,若是()说明这是一个函数名,到此处可知do_sth是一个指向函数的指针;若是[]说明这是一个数组,到此处可知,数组arr的每一个元素都是指向一个长度为20的字符数组的指针。
——2.3)看整体的前缀符号,星号*
表示“后面的所有字符整体是一个指针,指向前面的数据类型”。如例1表示该函数的返回值也是一个指针。
3、再看剩下的前面的修饰符数据类型说明,如上例1char * const
,表示该函数的返回值是一个类型为char的常量指针(该指针指向一个地址之后不能再指向其他地址,但是不管指向的地址内容如何改变)。若是const char *
或者char const *
都表示一个类型为指向只可读字符的指针(该指针只能用来读取地址中的数据,不能用来修改该数据,但是可以更改指向的地址)。
综上可知:
char * const * (*do_sth)(int *p);
——do_sth是一个指向一个函数的指针,该函数的返回值也是另一个指针,该指针指向一个char型的指针常量。函数接收的参数是一个指向int型的指针变量。
char (* arr[10])[20];
——arr是一个有10个元素的数组,每个元素都是一个指针,每个指针分别指向一个有20个char型元素的数组。
char * (* func[5])(char **str);
——func是一个有5个元素的数组,每个元素都是一个分别指向一个函数的指针,函数的返回值是一个指向char型的指针。函数的参数str是一个指向char *
(char型指针)类型的指针。
- 3、free指针来释放malloc开辟的链表的内存时,如果是用的头插法依次malloc的结点,在释放的时候,一定要记得释放头结点,否则容易造成内存泄漏释放不完全。尤其是在链表中每个结点内部还有头插法建立的链表时,一定要先把结点内部的链表都清空(特别注意别忘了头结点),然后才能释放这个节点。
break关键字使用错误
- 1、使用break可以跳出for、while等最近的一层循环和switch选择块,并经常是结合if-else判断后再执行break。但是,请注意,不要以为break可以跳出if-else代码块,否则将会出现很隐蔽的错误。
static关键字使用
- 1、用static修饰函数,可以限制这个函数在本源程序文件之外不可见。但是,这意味着缺省static时,函数都是其他文件可见的,很容易在链接其他文件编译时函数重名导致错误。遗憾的是,大部分人的习惯是懒得添加static关键字。也可以认为这是C语言设计上的一个错误,软件设计应该在大多数情况下缺省的采用有限可见性;当程序员需要让它全局可见,可以被其他文件使用时,再采用添加显示关键字的手段。在这一点上,java的private、缺省、protected、public,这几个关键字设计的就很合理,最常用的缺省表示整个package内可用。
- 2、用static修饰变量,表示该变量在作用域内的值在各个调用间一直保持延续性。常用于修饰全局变量,定义的时候就开闭一块固定的堆内存。
- 3、对应的有extern关键字,用于修饰函数时表示这个函数全局可见(冗余的,缺省这个也是一样的效果)。修饰变量时,表示这个变量在其他地方定义,这里只是把这个变量重新声明,并不重新定义并分配内存。(定义和声明的区别)
关于操作符优先级
- 只需牢记两个优先级就够了:乘法和除法的优先级高于加法和减法,在涉及其他的操作符时都一律根据个人想要的运算顺序加上括号。这是条很好的建议,可以避免很多错误出现。
- 优先级排序:() > !非(其他单目的) > 算术运算符 > 关系(> < == !=)运算符>逻辑(& ^ | && ||)运算符> 赋值运算符>逗号
malloc()函数使用注意
- 1、习惯性在每次malloc()函数开辟了内存后,习惯性后接一条语句
memset(ptr, 0, sizeof(内存块字节大小));
。
void *memset(void *s, int ch, size_t n);
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
》》》注意:
1)memset函数按字节对内存块进行初始化,每个字节是8bit位,所以ch的值不管取多大,也只能把二进制形式的后八位存到该字节内存。
2)int num; memset(&num, 1, sizeof(int));
因为是对每个字节赋值ch(这里是1),所以不要误以为这是给int型变量赋值1,真正的是给每个字节都赋值00000001,结果num的4字节存入的值为:00000001 00000001 00000001 00000001,换算成10进制就是num==16843009。
3)memset(ptr, 0, sizeof(内存块字节大小));
这一句不能简单的写成memset(ptr, 0, sizeof(ptr));
。sizeof(ptr)是求得一个指针(即一个地址)的大小,32位编译器默认一个地址大小是4个字节,并不是指的ptr指针指向的数组或结构体的内存大小。
union联合类型什么时候使用合适
- 1、union类型看起来和struct结构体类型用法很相似,但是,在内存布局上相差很大。struct中,每个成员依次存储;而在union中,所有成员都是从偏移地址0开始存储。也就是说所有成员的位置重叠了,因此每次只能有效使用一个成员。
- 2、一种常见的用法是:在一个结构体中使用联合类型作为一个成员,也就相当于每次只使用了联合中一个变量,好处是可以节省内存,因为联合中的成员都要是互斥的关系且数据类型大都不相同,每次只能出现一个成员。例如定义一个动物结构体,里面有个联合包含有毛皮和超过4条腿的总腿数这两个互斥事件,因为对于动物来说,要么是有脊椎的动物(有毛皮,不超过4条腿),要么是无脊椎动物(无毛皮,超过4条腿):
union secondary_characteristics{ /*第二特征,两个互斥的成员*/
char has_fur;
short num_of_legs_in_excess_of_4;
};
struct animals{
char has_backbone; /*是否有脊椎*/
union secondary_characteristics character;
};
这样做的好处是可以节省内存,如果程序运行期间,要存储几百万个动物,那么每个动物节省一个字节,也可以省下几百万字节,也就节省下几十兆的运行内存。
- 3、另一种用法是:把同一个数据解释成两种不同的东西,而不是把两个不同的数据解释为同一种东西,不需要额外的赋值或者强制类型装换来实现。如下代码所示,这个联合允许程序员提取整个32位的int值,也可以提取单独的字节字段如value32.byte.c1表示提取第二个字节的数据。
union bits32_tag{
int whole; /*一个32位的整型值*/
struct { char c0, c1, c2, c3; } byte; /*4个8位的字节*/
} value32;
分支语句switch—case注意点
- 1、switch后不管有多少个case,都是一个整体程序块,共享局部变量。并不是每个case作为一个程序块。因此,在case后定义局部变量会报错,可以用
case number: { //程序块 }
,即加上一对大括号来强行区分程序块。
if语句连续判断有讲究
- if条件判断时的顺序很重要,我们应该把大概率出现的事件先放到前面的if语句判断中,小概率发生的放到后面。这样做。大部分数据进来都会先在第一个判断结束,而不会进入后面的判断。这在一定程度上也是可以节约时间提高效率的。
- 《大话数据结构》p201页,由连续的if语句判断,引出二叉树分析,再引出哈夫曼树和哈夫曼压缩编码。
printf函数的参数从右往左执行再输出
int main()
{
int arr[] = { 0, 1, 2, 3, 4, 5 };
int *p_arr1 = arr;
printf("%d %d \n", *p_arr1, *(p_arr1++)); //print是从右往左执行再输出:1 0,而不是输出0 0
int *p_arr2 = arr;
printf("%d %d \n", *p_arr2, *(++p_arr2)); //print是从右往左执行再输出:1 1,而不是输出0 1
}