5
58
C 语言中的指针和内存泄漏
C 语言难点分析整理 10
C 语言难点 18
C/C++实现冒泡排序算法 32
C++中指针和引用的区别 35
const char*, char const*, char*const 的区别 36
C 中可变参数函数实现 38
C 程序内存中组成部分 41
C 编程拾粹 42
目录
1.
2.
3.
4.
5.
6.
7.
8.
9.
10. C 语言中实现数组的动态增长 44
11. C 语言中的位运算 46
12. 浮点数的存储格式: 50
13. 位域
14. C 语言函数二维数组传递方法 64
15. C 语言复杂表达式的执行步骤 66
16. C 语言字符串函数大全 68
17. C 语言宏定义技巧 89
18. C 语言实现动态数组 100
19. C 语言笔试-运算符和表达式 104
20. C 语言编程准则之稳定篇 107
21. C 语言编程常见问题分析 108
22. C 语言编程易犯毛病集合 112
23. C 语言缺陷与陷阱(笔记)
119
24. C 语言防止缓冲区溢出方法 126
25. C 语言高效编程秘籍 128
26. C 运算符优先级口诀 133
27. do/while(0)的妙用 134
28. exit()和 return()的区别 140
29. exit 子程序终止函数与 return 的差别 141
30. extern 与 static 存储空间矛盾 145
31. PC-Lint 与 C\C++代码质量 147
32. spirntf 函数使用大全 158
33. 二叉树的数据结构
167
34. 位运算应用口诀和实例
35. 内存对齐与 ANSI C 中 struct 内存布局 173
36. 冒泡和选择排序实现 180
37. 函数指针数组与返回数组指针的函数
38. 右左法则- 复杂指针解析 189
39. 回车和换行的区别
40. 堆和堆栈的区别 194
41. 堆和堆栈的区别 198
42. 如何写出专业的 C 头文件 202
43. 打造最快的 Hash 表 207
186
170
192
224
391
395
265
269
228
236
44. 指针与数组学习笔记 222
45. 数组不是指针
46. 标准 C 中字符串分割的方法
47. 汉诺塔源码 231
48. 洗牌算法
234
49. 深入理解 C 语言指针的奥秘
50. 游戏外挂的编写原理 254
51. 程序实例分析-为什么会陷入死循环 258
52. 空指针究竟指向了内存的哪个地方 260
53. 算术表达式的计算
54. 结构体对齐的具体含义
55. 连连看 AI 算法 274
56. 连连看寻路算法的思路
283
57. 重新认识:指向函数的指针 288
58. 链表的源码 291
59. 高质量的子程序 295
60. 高级 C 语言程序员测试必过的十六道最佳题目+答案详解 297
61. C 语言常见错误 320
62. 超强的指针学习笔记 325
63. 程序员之路──关于代码风格 343
64. 指针、结构体、联合体的安全规范 346
65. C 指针讲解 352
66. 关于指向指针的指针 368
67. C/C++ 误区一:void main()
373
68. C/C++ 误区二:fflush(stdin) 376
69. C/C++ 误区三:强制转换 malloc() 的返回值
70. C/C++ 误区四:char c = getchar();
381
71. C/C++ 误区五:检查 new 的返回值 383
72. C 是 C++ 的子集吗? 384
73. C 和 C++的区别是什么? 387
74. 无条件循环 388
75. 产生随机数的方法
76. 顺序表及其操作 390
77. 单链表的实现及其操作
78. 双向链表
79. 程序员数据结构笔记 399
80. Hashtable 和 HashMap 的区别 408
81. hash 表学习笔记 410
82. C 程序设计常用算法源代码 412
83. C 语言有头结点链表的经典实现 419
84. C 语言惠通面试题 428
85. C 语言常用宏定义 450
380
389
1.
C 语言中的指针和内存泄漏
在使用 C 语言时,您是否对花时间调试指针和内存泄漏问题感到厌倦?如果是这样,那么
本文就适合您。您将了解可能导致内存破坏的指针操作类型,您还将研究一些场景,了解要
在使用动态内存分配时考虑什么问题。
引言
对于任何使用 C 语言的人,如果问他们 C 语言的最大烦恼是什么,其中许多人可能会回答
说是指针和内存泄漏。这些的确是消耗了开发人员大多数调试时间的事项。指针和内存泄漏
对某些开发人员来说似乎令人畏惧,但是一旦您了解了指针及其关联内存操作的基础,它们
就是您在 C 语言中拥有的最强大工具。
本文将与您分享开发人员在开始使用指针来编程前应该知道的秘密。本文内容包括:
1. 导致内存破坏的指针操作类型
2. 在使用动态内存分配时必须考虑的检查点
3. 导致内存泄漏的场景
如果您预先知道什么地方可能出错,那么您就能够小心避免陷阱,并消除大多数与指针和内
存相关的问题。
? 什么地方可能出错?
有几种问题场景可能会出现,从而可能在完成生成后导致问题。在处理指针时,您可以使用
本文中的信息来避免许多问题。
1. 未初始化的内存
在本例中,p 已被分配了 10 个字节。这 10 个字节可能包含垃圾数据,如图 1 所示。
char *p = malloc ( 10 );
图 1. 垃圾数据
如果在对这个 p 赋值前,某个代码段尝试访问它,则可能会获得垃圾值,您的程序可能具
有不可预测的行为。p 可能具有您的程序从未曾预料到的值。
良好的实践是始终结合使用 memset 和 malloc,或者使用 calloc。
char *p = malloc (10);memset(p,’\0’,10);
现在,即使同一个代码段尝试在对 p 赋值前访问它,该代码段也能正确处理 Null 值(在
理想情况下应具有的值),然后将具有正确的行为。
2. 内存覆盖
由于 p 已被分配了 10 个字节,如果某个代码片段尝试向 p 写入一个 11 字节的值,则该
操作将在不告诉您的情况下自动从其他某个位置“吃掉”一个字节。让我们假设指针 q 表
示该内存。
图 2. 原始 q 内容
图 3. 覆盖后的 q 内容
结果,指针 q 将具有从未预料到的内容。即使您的模块编码得足够好,也可能由于某个共
存模块执行某些内存操作而具有不正确的行为。下面的示例代码片段也可以说明这种场景。
Char *name = (char *) malloc(11); // Assign some value to namememcpy ( p,name,11);
// Problem begins here
在本例中,memcpy 操作尝试将 11 个字节写到 p,而后者仅被分配了 10 个字节。
作为良好的实践,每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉
核对。一般情况下,memcpy 函数将是用于此目的的检查点。
内存读取越界
内存读取越界 (overread) 是指所读取的字节数多于它们应有的字节数。这个问题并不太严
重,在此就不再详述了。下面的代码提供了一个示例。
char *ptr = (char *)malloc(10);char name[20] ;memcpy ( name,ptr,20); // Problem
begins here
在本例中,memcpy 操作尝试从 ptr 读取 20 个字节,但是后者仅被分配了 10 个字节。
这还会导致不希望的输出。
内存泄漏
内存泄漏可能真正令人讨厌。下面的列表描述了一些导致内存泄漏的场景。
重新赋值
我将使用一个示例来说明重新赋值问题。
char *memoryArea = malloc(10);char *newArea = malloc(10);
这向如下面的图 4 所示的内存位置赋值。
图 4. 内存位置
memoryArea 和 newArea 分别被分配了 10 个字节,它们各自的内容如图 4 所示。如果某
人执行如下所示的语句(指针重新赋值)…… memoryArea = newArea;
则它肯定会在该模块开发的后续阶段给您带来麻烦。
在 上 面 的 代 码 语 句 中 , 开 发 人 员 将 memoryArea 指 针 赋 值 给 newArea 指 针 。 结 果 ,
memoryArea 以前所指向的内存位置变成了孤立的,如下面的图 5 所示。它无法释放,因为
没有指向该位置的引用。这会导致 10 个字节的内存泄漏。
图 5. 内存泄漏
在对指针赋值前,请确保内存位置不会变为孤立的。
首先释放父块
假设有一个指针 memoryArea,它指向一个 10 字节的内存位置。该内存位置的第三个字节
又指向某个动态分配的 10 字节的内存位置,如图 6 所示。
图 6. 动态分配的内存
free(memoryArea)
如果通过调用 free 来释放了 memoryArea,则 newArea 指针也会因此而变得无效。newArea
以前所指向的内存位置无法释放,因为已经没有指向该位置的指针。换句话说,newArea 所
指向的内存位置变为了孤立的,从而导致了内存泄漏。
每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子
内存位置(在此例中为 newArea),并从那里开始释放,然后再遍历回父节点。
这里的正确实现应该为:
free( memoryArea->newArea);free(memoryArea);
返回值的不正确处理
有时,某些函数会返回对动态分配的内存的引用。跟踪该内存位置并正确地处理它就成为了
calling 函数的职责。
char *func ( ){
return malloc(20); // make sure to memset this location to ‘\0’…}
func ( ); // Problem lies here}
void callingFunc ( ){
在上面的示例中,callingFunc() 函数中对 func() 函数的调用未处理该内存位置的返回地
址。结果,func() 函数所分配的 20 个字节的块就丢失了,并导致了内存泄漏。
归还您所获得的
在开发组件时,可能存在大量的动态内存分配。您可能会忘了跟踪所有指针(指向这些内存
位置),并且某些内存段没有释放,还保持分配给该程序。
始终要跟踪所有内存分配,并在任何适当的时候释放它们。事实上,可以开发某种机制来跟
踪这些分配,比如在链表节点本身中保留一个计数器(但您还必须考虑该机制的额外开销)。
访问空指针
访问空指针是非常危险的,因为它可能使您的程序崩溃。始终要确保您不是在访问空指针。
总结
本文讨论了几种在使用动态内存分配时可以避免的陷阱。要避免内存相关的问题,良好的实
践是:
始终结合使用 memset 和 malloc,或始终使用 calloc。
每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。
在对指针赋值前,要确保没有内存位置会变为孤立的。
每当释放结构化的元素(而该元素又包含指向动态分配的内存位置的指针)时,都应首先遍
历子内存位置并从那里开始释放,然后再遍历回父节点。
始终正确处理返回动态分配的内存引用的函数返回值。
每个 malloc 都要有一个对应的 free。
确保您不是在访问空指针。
C 语言难点分析整理
2.
这篇文章主要是介绍一些在复习 C 语言的过程中笔者个人认为比较重点的地方,较好的掌握
这些重点会使对 C 的运用更加得心应手。此外会包括一些细节、易错的地方。涉及的主要内
容包括:变量的作用域和存储类别、函数、数组、字符串、指针、文件、链表等。一些最基
本的概念在此就不多作解释了,仅希望能有只言片语给同是 C 语言初学者的学习和上机过程
提供一点点的帮助。
变量作用域和存储类别:
了解了基本的变量类型后,我们要进一步了解它的存储类别和变量作用域问题。
变量类别
局部变量
子类别
静态变量(离开函数,变量值仍保留)
自动变量
寄存器变量
全局变量
静态变量(只能在本文件中用)
非静态变量(允许其他文件使用)
换一个角度
变量类别
静态存储变量
子类别
静态局部变量(函数)
静态全局变量(本文件)
非静态全局/外部变量(其他文件引用)
动态存储变量
自动变量
寄存器变量
形式参数
extern 型的存储变量在处理多文件问题时常能用到,在一个文件中定义 extern 型的变量即
说明这个变量用的是其他文件的。顺便说一下,笔者在做课设时遇到 out of memory 的错误,
于是改成做多文件,再把它 include 进来(注意自己写的*.h 要用“”不用<>),能起到一
定的效用。static 型的在读程序写结果的试题中是个考点。多数时候整个程序会出现多个
定义的变量在不同的函数中,考查在不同位置同一变量的值是多少。主要是遵循一个原则,
只要本函数内没有定义的变量就用全局变量(而不是 main 里的),全局变量和局部变量重名
时局部变量起作用,当然还要注意静态与自动变量的区别。
函数:
对于函数最基本的理解是从那个叫 main 的单词开始的,一开始总会觉得把语句一并写在
main 里不是挺好的么,为什么偏择出去。其实这是因为对函数还不够熟练,否则函数的运
用会给我们编程带来极大的便利。我们要知道函数的返回值类型,参数的类型,以及调用函
数时的形式。事先的函数说明也能起到一个提醒的好作用。所谓形参和实参,即在调用函数
时写在括号里的就是实参,函数本身用的就是形参,在画流程图时用平行四边形表示传参。
函数的另一个应用例子就是递归了,笔者开始比较头疼的问题,反应总是比较迟钝,按照老
师的方法,把递归的过程耐心准确的逐级画出来,学习的效果还是比较好的,会觉得这种递
归的运用是挺巧的,事实上,著名的八皇后、汉诺塔等问题都用到了递归。
例子:
longfun(intn)
{
longs;
if(n==1||n==2) s=2;
elses=n-fun(n-1);
returns;
}
main()
{
printf("%ld",fun(4));
}
数组:
分为一维数组和多维数组,其存储方式画为表格的话就会一目了然,其实就是把相同类型的
变量有序的放在一起。因此,在处理比较多的数据时(这也是大多数的情况)数组的应用范
围是非常广的。
具体的实际应用不便举例,而且绝大多数是与指针相结合的,笔者个人认为学习数组在更大
程度上是为学习指针做一个铺垫。作为基础的基础要明白几种基本操作:即数组赋值、打印、
排序(冒泡排序法和选择排序法)、查找。这些都不可避免的用到循环,如果觉得反应不过
来,可以先一点点的把循环展开,就会越来越熟悉,以后自己编写一个功能的时候就会先找
出内在规律,较好的运用了。另外数组做参数时,一维的[]里可以是空的,二维的第一个[]
里可以是空的但是第二个[]中必须规定大小。
冒泡法排序函数:
voidbubble(int a[],int n)
{
inti,j,k;
for(i=1,i
for(j=0;j
a[j+1])
{
k=a[j];
a[j]=a[j+1];
a[j+1]=k;
}
}
选择法排序函数:
voidsort(int a[],int n)
{
inti,j,k,t;
for(i=0,i等等。
字符串:
字符串其实就是一个数组(指针),在 scanf 的输入列中是不需要在前面加“&”符号的,因
为字符数组名本身即代表地址。值得注意的是字符串末尾的‘’,如果没有的话,字符串很
有可能会不正常的打印。另外就是字符串的定义和赋值问题了,笔者有一次的比较综合的上
机作业就是字符串打印老是乱码,上上下下找了一圈问题,最后发现是因为
char*name;
而不是
charname[10];
前者没有说明指向哪儿,更没有确定大小,导致了乱码的错误,印象挺深刻的。
另外,字符串的赋值也是需要注意的,如果是用字符指针的话,既可以定义的时候赋初值,
即
char*a="Abcdefg";
也可以在赋值语句中赋值,即
char*a;
a="Abcdefg";
但如果是用字符数组的话,就只能在定义时整体赋初值,即 char a[5]={"abcd"};而不能在
赋值语句中整体赋值。
常用字符串函数列表如下,要会自己实现:
函数作用
字符串拷贝函数 strcpy(char*,char *)
字符串追加函数 strcat(char*,char *)
要足够大
字符串比较函数 strcmp(char*,char *)
负值。注意,不是比较长度,是比较字符 ASCII 码的大小,可用于按姓名字母排序等。
字符串长度 strlen(char *) 返回字符串的长度,不包括''.转义字符算一个字符。
字符串型->整型 atoi(char *)
整型->字符串型 itoa(int,char *,int)
后者拷贝到前者
后者追加到前者后,返回前者,因此前者空间
前者等于、小于、大于后者时,返回 0、正值、
函数调用形式
备注
做课设时挺有用的
sprintf(char *,格式化输入) 赋给字符串,而不打印出来。课设时用也比较方便
注:对字符串是不允许做==或!=的运算的,只能用字符串比较函数
指针:
指针可以说是 C 语言中最关键的地方了,其实这个“指针”的名字对于这个概念的理解是十
分形象的。首先要知道,指针变量的值(即指针变量中存放的值)是指针(即地址)。指针
变量定义形式中:基本类型 *指针变量名 中的“*”代表的是这是一个指向该基本类型的指
针变量,而不是内容的意思。在以后使用的时候,如*ptr=a 时,“*”才表示 ptr 所指向的
地址里放的内容是 a。
指针比较典型又简单的一应用例子是两数互换,看下面的程序,
swap(intc,intd)
{
intt;
t=c;
c=d;
d=t;
}