深入理解C指针----学习笔记

 
深入理解C指针
 
 
第1章 认识指针
 
理解指针的关键在于理解C程序如何管理内存,指针包含的就是内存地址。
 
 
1.1 指针和内存
 
C程序在编译后,以三种方式使用内存:
1. 静态、全局内存
在程序开始运行时分配,直到程序终止才消失。所有函数都能访问全局变量,静态变量的作用域则局限在定义它们的函数内部。
 
2. 自动变量
在函数内部声明,在函数被调用时才创建。作用域局限于函数内部,而且生命周期局限在函数的执行时间内。
 
3. 动态内存
动态内存分配在堆中,可根据需要释放,直到释放才消失。指针引用分配的内存,作用域局限于引用内存的指针。
 
不同内存中变量的作用域和生命周期
 
内存类型 作用域 生命周期
全局内存 整个文件 应用程序的生命周期
静态内存 声明它的函数内部 应用程序的生命周期
自动内存(局部内存) 声明它的函数内部 限制在函数执行时间内
动态内存 由引用该内存的指针决定 直到内存释放
 
 
 
第2章 C的动态内存管理
 
 
内存管理:
     自动变量,内存在它所处的函数的栈帧上;
     静态或全局变量,内存处于程序的数据段,会被自动清零;
     
注:
     C99引入了变长数组(VLA)。数组长度不是在编译时确定,而是在运行时确定。不过,数组一旦创建出来就不能再改变长度了。
 
动态内存管理:
     用分配和释放函数手动实现。
 
 
2.1 动态内存分配
 
动态分配内存的基本步骤:
1) 用malloc类的函数分配内存
2) 用这些内存支持应用程序
3) 用free函数释放内存
 
int *pi = (int *)malloc(sizeof(int) * 1);
*pi = 5;
...
free(pi);
 
malloc 函数的参数指定要分配的字节数。
     如果成功,返回从堆上分配的内存的指针。
     如果失败,返回空指针。
 
注:
     每次调用malloc (或类似函数),程序结束时必须有对应的free函数调用,以防止内存泄漏。
 
     内存被释放后,就不应该再访问了,为防止这种错误,通常的做法是把被释放的指针赋值为NULL。
 
     分配内存时,堆管理器维护的数据结构中会保存额外的信息,包括块大小和其他信息,通常放在紧挨分配块的内存中。如果写入操作超出了动态分配的内存块,即越界写入,数据结构会被破坏。
 
     内存泄漏
 
如果不再使用已分配的内存,却没有将其释放,就会发生内存泄漏,导致内存泄漏的情况可能如下:
1) 丢失内存地址
2) 应该调用free函数却没有调用(隐式泄漏)
 
1. 丢失地址
 
     在需要进行指针偏移的地方,原来指向动态分配内存的指针发生了偏移,丢失内存块的起始地址。
 
2. 隐式内存泄漏
 
     如果程序应该释放内存,而实际却没有释放,会发生内存泄漏。
 
     在释放用struct关键字创建的结构体时,可能发生内存泄漏。如果结构体包含指向动态分配的内存的指针,就需要在释放结构体之前先释放这些指针。
 
 
2.2 动态内存分配函数
 
 
2.3 用free函数释放内存
 
 
 
2.4 迷途指针
 
 
 
2.5 动态内存分配技术
 
 
 
2.6 小结
 
 
     
第3章 指针和函数
 
使用函数时,两种情况指针很有用:
1. 将指针传递给函数
     这时函数可以修改指针所引用的数据,也可以更高效地传递大块信息。
2. 声明函数指针
     函数表示法就是指针表示法,函数名字经过求值会变成函数的地址,然后函数参数被传递给函数,函数指针可以很好地控制程序的执行流。
 
3.1 程序的栈和堆
 
局部变量,也称为自动变量,被分配在栈帧上。
 
1. 程序栈
程序栈存放栈帧,栈帧存放函数参数和局部变量。堆管理动态内存。
栈在下部,向上长出,栈帧在数据弹出后,内存并不会被清理。
堆在上部,向下生长,碎片。
 
2. 栈帧的组织
系统在创建栈帧时,将函数参数以跟声明时相反的顺序推到帧上,接下来推入函数调用的返回地址,最后推入局部变量。
 
每个程序都有自己的程序栈。一个或多个线程访问内存中同一个对象可能会导致冲突。
 
 
3.2 通过指针传递和返回数据
 
传递参数(包括指针)时,传递的是它们的值。即,传递给函数的是参数值的一个副本。
传递对象的指针意味着不需要复制对象,但可以通过指针访问对象。
 
1. 用指针传递数据
 
一个主要原因是函数可以修改数据。
 
void swap(int *num1, int *num2)
{
     int tmp;
     tmp = *num1;
     *num1 = *num2;
     *num2 = tmp;
}
 
swap(&data1, &data2);
 
2. 用值传递数据
 
     在函数中修改传递的数据,不会影响函数外的数据,因为在函数中修改的是形参,修改形参不会影响实参。
 
3. 传递指向常量的指针
 
     传递指向常量的指针,效率很高,因为只传递了数据的地址,能避免某些情况下复制大量内存。传递指针,数据就能够被修改,如果不希望数据被修改,就要传递指向常量的指针。
 
4. 返回指针
 
从函数返回对象时,有两种:
     1. 使用malloc在函数内部分配内存并返回其地址。调用者负责释放返回的内存。
     2. 传递一个对象给函数并让函数修改它。
 
 
用函数为整数数组分配内存:
 1 int *mallocArray(int size, int value)
 2 {
 3      int i = 0;
 4      int *arr = (int *)malloc(size * sizeof(int));
 5      for (i=0; i<size; i++)
 6      {
 7           arr[i] = value; 
 8      }
 9  
10      return arr;
11 }
12  
13 int *vector = mallocArray(5, 0);
14 ...
15 free(vector);
从函数返回指针时可能存在几个潜在问题:
     返回未初始化的指针;
     返回指向无效地址的指针;
     返回局部变量的指针;
     返回指针但是没有释放内存。
 
从函数返回动态分配的内存意味着函数的调用者有责任释放内存,否则就会产生内存泄漏。
 
5. 局部数据指针
 
函数返回指向局部数据的指针,一旦函数返回,局部数据空间就被释放,在函数外使用会发生错误。
 
     把变量声明为static,会把变量的作用域限制在函数内部,分配在栈帧外面,避免其它函数覆写变量值。不过每次调用该函数就会重复利用该变量,会覆盖上个数据。
 
6. 传递空指针
 
将指针传递给函数时,使用之前先判断它是否为空是个好习惯。
 
7. 传递指针的指针
 
将指针传递给函数时,传递的是值。如果要修改原指针而不是指针的副本,就需要传递指针的指针。
 
实现自己的free()函数:
free函数不会检查传入的指针是否是NULL,也不会在返回前把指针置为NULL。
释放指针之后将其置为NULL是个好习惯。
 
 1 void saferFree(void **pp)  //调用时需要将指针类型显示地强制转换为void
 2 {
 3      if (pp != NULL && *pp != NULL)
 4      {
 5           free(*pp);
 6           *pp = NULL;
 7      }
 8 }
 9  
10 #define     saferFree(p)     saferFree((void **)&p)
 
 
3.3 函数指针
 
 
     函数指针是持有函数地址的指针。为以编译时未确定的顺序执行函数提供了方便,不需要使用条件语句。
 
1. 声明函数指针
 
void (*foo)();
 
第一个括号让这个声明变成了一个名为foo的函数指针。星号表示这是个指针。
 
 
注:
     使用函数指针时一定要小心,因为C不会检查参数传递是否正确。
     对函数指针在命名约定上建议用 fptr 做前缀。
 
2. 使用函数指针
 
     为函数指针声明一个类型定义。
 
1 typedef int (*funcptr)(int);
2 int square(int, int);
3 ...
4  
5 funcptr fptr;
6 fptr = square;
7 printf("%d squared is %d
", n, fptr(n));
 
3. 传递函数指针
 
  1 #include <stdio.h>
  2
  3 int sum(int num1, int num2)
  4 {
  5     return num1 + num2;
  6 }
  7
  8 int sub(int num1, int num2)
  9 {
 10     return num1 - num2;
 11 }
 12
 13 typedef (*fptrOperation)(int, int);
 14
 15 int computer(fptrOperation operation, int num1, int num2)
 16 {
 17     printf("num1 = %d  num2 = %d  resule = %d
", num1, num2, operation(num1, num2));
 18     return ;
 19 }
 20
 21 int main(int argc, char *argv[])
 22 {
 23     int num1;
 24     int num2;
 25
 26     printf("Please input two numbers:
");
 27     scanf("%d %d", &num1, &num2);
 28
 29     computer(sum, num1, num2);
 30     computer(sub, num1, num2);
 31
 32     return 0;
 33 }
 
4. 返回函数指针
 
返回函数指针需要把函数的返回类型声明为函数指针。
 
  1 #include <stdio.h>
  2
  3 int sum(int num1, int num2)
  4 {
  5     return num1 + num2;
  6 }
  7
  8 int sub(int num1, int num2)
  9 {
 10     return num1 - num2;
 11 }
 12
 13 typedef (*fptrOperation)(int, int);
 14
 15 fptrOperation select(char opcode)
 16 {
 17     switch(opcode)
 18     {
 19         case '+':
 20             return sum;
 21         case '-':
 22             return sub;
 23         default:
 24             printf("Wrong operation code!
");
 25             break;
 26     }
 27 }
 28
 29 int evaluate(char opcode, int num1, int num2)
 30 {
 31     fptrOperation operation = select(opcode);
 32     return operation(num1, num2);
 33 }
 34
 35 int main(int argc, char *argv[])
 36 {
 37     int num1;
 38     int num2;
 39
 40     printf("Please input two numbers:
");
 41     scanf("%d %d", &num1, &num2);
 42
 43     printf("num1 = %d  num2 = %d  resule = %d
", num1, num2, evaluate('+', num1, num2));
 44     printf("num1 = %d  num2 = %d  resule = %d
", num1, num2, evaluate('-', num1, num2));
 45
 46     return 0;
 47 }
 
5. 使用函数指针数组
 
定义及初始化:
 
typedef int (*operation)(int, int);
operation operations[128] = {NULL};
 
int (*operations[128])(int, int) = {NULL};
 
6. 比较函数指针
 
     可以用相等和不等操作符来比较函数指针。
 
fptrOperation fptr = add;
 
if (fptr == add)
{
     ...;
}
 
7. 转换函数指针
 
     可以将指向某个函数的指针转换为其他类型的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。转换后函数指针的长度不一定相同。
 
注:
     无法保证函数指针和数据指针相互转换后能正常工作。
     void * 指针不一定能用在函数指针上。
     一定要确保给函数指针传递正确的参数。
 
3.4 小结
 
 
第4章 指针和数组
 
常见的错误观点:数组和指针是可以完全互换的。  错误!!!
 
数组使用自身的名字可以返回数组地址,但是名字本身不能作为赋值操作的目标。
 
     声明数组时需要指定该数组有多大,有多少个元素。数组索引从0开始,到声明的长度-1结束。用无效的索引访问数组会造成不可预期的行为。
 
数组元素个数 = 数组长度 除以 元素长度(size = sizeof(array) / sizeof(array[0])))。
 
n = sizeof(array) / sizeof(array[0]);
//或者
n = sizeof(array) / sizeof(int);
 
 
第6章 指针和结构体
 
 
6.4 用指针支持数据结构
 
2. 用指针支持队列
 
用链表实现队列
先进先出(FIFO),第一个进入队列的元素,第一个被取出。
最常见操作:入队,出队
 
 
3. 用指针支持栈
 
用链表实现栈
先进后出(FILO),第一个进入栈的元素,最后一个被取出。
最常见操作:入栈,出栈
 
4. 用指针支持树
 
用链表实现二叉树
子节点连接到父节点,从整体上看就像一颗倒过来的树,根节点表示这种数据结构的开始元素。
常见的二叉树,每个节点可能有0、1、2个子节点,子节点要么是左节点,要么是右节点,
二叉查找树,插入新节点后,这个节点的所有左子节点的值都比父节点小,所有右子节点的值都比父节点大。
typedef struct _tree {
     void *data;
     struct _tree *left;
     struct _tree *right;
} TreeNode;
 
遍历二叉树的方式有三种:前序、中序、后序。
 
 
第7章 安全问题和指针误用
 
 
7.1 处理未初始化指针
  
int *pi = NULL;
...
if (pi == NULL)
{
     //空指针
}
else
{
     //指针可以使用
}
 
     可以用assert()函数测试指针是否为空值,如果表达式为真,什么也不会发生,如果表达式为假,程序会终止,即指针为空程序会终止。
 
assert(pi != NULL);
//如果指针为空值,输出:
Assertion failed: pi != NULL
 
 
7.2 指针的使用问题
 
可能导致缓冲区溢出的几种情况:
  1. 访问数组元素时没有检查索引值
  2. 对数组指针做指针算术运算时不够小心
  3. 用gets这样的函数从标准输入读取字符串
  4. 误用strcpy和strcat这样的函数
 
用malloc这类函数时一定要检查返回值,否则可能会导致程序非正常终止。
 
int *vector = malloc(20 * sizeof(int));
if (vector == NULL)
{
     //malloc分配内存失败
}
else
{
     //处理vector
}
 
 
3. 迷途指针
 
     释放指针后仍然在引用原来的内存。
 
4. 越过数组边界访问内存
 
5. 错误计算数组长度
 
将数组传递给函数时,一定要同时传递数组长度,有助于函数避免越过数组边界。
 
6. 错误使用sizeof操作符
 
sizeof(int) = 4;
sizeof(char) = 1;
//易搞混
//常用 sizeof(buffer) / sizeof(int) 来代替buffer元素的个数
 
 
7. 要匹配指针类型
 
用合适的指针类型来装数据,再指针类型强制转换时一定要注意。
 
8. 有界指针
 
有界指针是指指针的使用被限制在有效的区域内。如,禁止对数组使用的指针访问数组前面或后面的任何内存。
 
9. 字符串
 
字符串相关的安全问题,一般发生在越过字符串末尾写入的情况。
使用strcpy和strcat这类字符串函数,容易引发缓冲区溢出。
printf、fprintf、snprintf、syslog这些函数都接受格式化字符串作为参数,避免这类攻击的一种简单方法是永远不要把用户提供的格式化字符串传递给这些函数。
 
10. 指针算术运算和结构体
 
     应该只对数组使用指针算术运算,因为数组肯定分配在连续的内存块上,指针算术运算可以得到有效的偏移量。不应该对结构体使用指针算术运算,因为结构体的字段可能分配在不连续的内存区域。
 
在结构体中尽量不要使用指针算术运算。
 
11. 函数指针问题
 
函数和函数指针用来控制程序的执行顺序。
只用函数名本身,而不加函数名后的括号时,调用的是函数的地址。
函数指针可以执行不同的函数,这取决于分配给它的地址。
 
 
 
7.3 内存释放问题
 
1. 重复释放
 
避免将同一块内存释放多次的简单方法是,释放指针后总是将其置为NULL。
 
char *name = (char *)malloc(...);
...
free(name);
name = NULL;
 
 
2. 清除敏感数据
 
当应用程序终止后,大部分操作系统都不会把用到的内存清空或者执行别的操作。系统可能会将之前用过的写有敏感数据的空间分配给别的程序,别的程序就能访问内存中的敏感数据。
应该在不需要使用内存时,将内存清空。
 
 
char name[32];
int userID;
char *string = (char *)malloc(...);
...
//删除敏感信息
memset(name, 0, sizeof(name));
userID = 0;
memset(string, 0, sizeof(string));
free(string);
string = NULL;
 
 
7.4 使用静态分析工具
 
可以使用GCC编译器的-Wall选项启用编译器警告。
 
7.5 总结
 
指针影响程序的安全性和可靠性的问题,基本上都是围绕声明和初始化指针、使用指针和释放内存组织的。
 
 
第8章 重要内容
 
8.1 转换指针
 
两种常见的字节序:大字节序和小字节序,即大小端。
大字节序:将低位字节存储在低地址中;
小字节序:将高位字节存储在低地址中。
 
1. 访问特殊用途的地址
 
底层内核需要访问地址0,有几种方法:
  1. 把指针置为0(可能被识别为NULL)
  2. 把整数置为0,再把这个整数转换为指针
  3. 联合体
  4. 用memset()函数把指针置为0
memset((void *)&ptr, 0, sizeof(ptr));
 
2. 访问端口
 
端口即硬件概念
 
如何用指针访问端口:
 
#define PORT 0xB0000000;
unsigned int volatile * const port = (unsigned int *)PORT;
*port = 0x0001;
value = *port; 
机器使用十六进制地址表示端口,将数据作为无符号整数处理。
volatile关键字可以阻止运行时系统使用寄存器暂存端口值,每次访问端口都需要系统读写端口,而不是从寄存器读取一个旧值。
 
 
4. 判断机器的字节序
 
可以使用类型转换操作来判断架构的字节序。
字节序:内存单元中字节存储的顺序。
 
把整数的地址从指针转换为char,可以判断字节序:
 
int num = 0x12345678;
char *pc = (char *)&num;
for (i = 0; i < 4; i++)
{
     printf("%p: %02x ", pc, (unsigned char) *pc++);
}
 
 
8.2 别名、强别名和restrict关键字
 
别名:如果两个指针引用同一内存地址,称一个指针是另一个指针的别名。
强别名:是另一种别名,它不允许一种类型的指针成为另一种类型的指针的别名。
 
为了避免别名问题,可以采用以下方法:
使用联合体
关闭强别名
使用char指针
 
GCC编译器的编译器选项:
     -fno-strict-aliasing     关闭强别名
     -fstrict-aliasing     打开强别名
     -Wstrict-aliasing     打开跟强别名相关的警告信息
 
编译器总是假定char指针是任意对象的潜在别名。
 
1. 用联合体以多种方式表示值
 
C语言有时候需要把一种类型转换为另一种类型,一般通过类型转换实现。也可以使用联合体。
 
2. 强别名
 
     即使两个结构体的字段完全一样,但如果名字不同的话,这两种结构体的指针就不应该引用同一对象。不过,如果定义了同一个结构体的两种类型,那么指向不同名字的指针可以引用同一对象。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/microxiami/p/7864896.html