指针基础
变量回顾
既然程序中的变量只是一段存储空间的别名 , 那么是不
是必须通过这个别名才能使用这段存储空间 ?
指针本质
? 指针在本质上也是一个变量
? 指针需要占用一定的内存空间
? 指针用于保存内存地址的值
* 号的意义
? 在指针声明时 ,* 号表示所声明的变量为指针
? 在指针使用时 ,* 号表示取指针所指向的内存空间中的值
// 指针声明 :
int i = 0;
int j = 0;
int* p = &i;
// 取值 :
j = *p;
p: 0xAABBCCD0 i: 0
0xAABBCCD0
* 号类似一把钥匙 , 通过这把钥匙可以
打开内存 , 读取内存中的值 。
1. 指针占用的内存空间
2. 指针的地址
3. 通过* 号写内存
传值调用与传址调用
? 指针是变量 , 因此可以声明指针参数
? 当一个函数体内部 需要改变实参的值 , 则需要 使用指针参数
? 函数调用时实参值将复制到形参
? 指针适用于复杂数据类型作为参数的函数中
常量与指针(判断const修饰的是p还是*p)
? const int* p; //p 可变 ,p 指向的内容不可变
? int const* p; //p 可变 ,p 指向的内容不可变
? int* const p; //p 不可变 ,p 指向的内容可变
? const int* const p; //p 和p 指向的内容都不可变
口诀 : 左数右指
当const 出现在* * 号左边时指针指向的数据为常量
当const 出现在* * 后右边时指针本身为常量
www.enjoylinux.cn
指针小结
? 指针是C 语言中一种特别的变量
? 指针所保存的值是内存的地址
? 可以通过指针修改内存中的任意地址内容
int* p
0x00eaff00 10 …… …… ……
0x00eaff00
数组基础
数组的概念
数组是 相同类型 的变量的 有序集合
int a[5];
a代表数组第一个元素的起始地址。
这20个字节空间的名字为
a。a[0], a[1]等都是a中的
元素,并非元素的名字。数
组中的元素没有名字。
每个元素都是int型数据
数组的大小
? 数组在一片连续的内存空间中存储元素
? 数组元素的个数可以显示或隐式指定
数组地址与数组名
? 数组名代表数组首元素的地址,是个常量(整数),因此,不能作为左值
? 数组的地址需要用取地址符 & 才能得到
? 数组首元素的地址值与数组的地址值相同
? 数组首元素的地址与数组的地址是两个不同的概念
你家所在的楼房和你家的 GPS 地址是
相同的 , 但意义相同吗 ?
数组名的盲点
? 数组名可以看做一个常量指针
? 数组名“ 指向” 的是内存中数组首元素的起始位置
? 在表达式中数组名只能作为右值使用
? 只有在下列场合中数组名不能看做常量指针
? 数组名作为sizeof 操作符的参数
? 数组名作为& 运算符的参数
数组小结
? 数组是一片连续的内存空间
? 数组的地址和数组首元素的地址意义不同
? 数组名在大多数情况下被当成常量指针处理
? 数组名其实并不是指针 , 在外部声明时不能混淆
概念的混淆是 BUG 的根源之一 !
数组与指针分析
数组的本质
? 数组是一段连续的内存空间
? 数组的空间大小为sizeof(array_type) * array_size
? 数组名可看做指向数组第一个元素的常量指针
指针的运算
? 指针是一种特殊的变量 , 与整数的运算规则为
p + n; ?? (unsigned int)p + n*sizeof(*p);
结论 :
当指针p 指向一个同类型的数组的元素时 : p+1 将指向
当前元素的下一个元素 ; p- 1 将指向当前元素的上一
个元素 。
指针的运算
? 指针之间只支持减法运算 , 且必须参与运算的指针类型必
须相同
p1 – p2; ( (unsigned int)p1 - (unsigned int)p2) / sizeof(type);
注意 :
? 只有当两个指针指向同一个数组中的元素时 , 指针
相减才有意义 , 其意义为指针所指元素的下标差
? 当两个指针指向的元素不在同一个数组中时 , 结果
未定义
指针的比较
? 指针也可以进行关系运算
< <= > >=
? 指针关系运算的前提是同时指向同一个数组中的元素
? 任意两个指针之间的比较运算(==, !=) 无限制
数组的访问
? 以 下标的形式 访问数组中的元素
? 以 指针的形式 访问数组中的元素
标 下标 VS 指针
? 从理论上而言 , 当指针以固定增量在数组中移动时 , 其效
率高于下标产生的代码
? 当指针增量为1 且硬件具有硬件增量模型时 , 表现更佳
注意 :
现代编译器的生成代码优化率已大大提高 , 在固定增
量时 , 下标形式的效率已经和指针形式相当 ; 但从可
读性和代码维护的角度来看 , 下标形式更优 。
a 和&a 的区别
?a 为数组是数组首元素的地址
?&a 为整个数组的地址
?a 和&a 的意义不同其区别在于指针运算
a + 1 (unsigned int)a + sizeof(*a)
&a + 1 (unsigned int)(&a) + sizeof(*&a)
数组参数
?C 语言中 , 数组作为函数参数时 , 编译器将
其编译成对应的指针
void f(int a[]); ?? void f(int* a);
void f(int a[5]); ?? void f(int* a);
结论 :
一般情况下 , 当定义的函数中有数组参数时 , 需要定
义另一个参数来标示数组的大小 。
指针和数组的对比
? 数组声明时编译器自动分配一片连续内存空间
? 指针声明时只分配了用于容纳指针的4 字节空间
? 在作为函数参数时 , 数组参数和指针参数等价
? 数组名在多数情况可以看做常量指针 , 其值不能改变
? 指针的本质是变量 , 保存的值被看做内存中的地址
C 语言中的字符串
? 从概念上讲 ,C 语言中没有字符串数据类型? 在C 语言中使用 字符数组来模拟字符串
? C 语言中的字符串是以’\0’ 结束的字符数组
? C 语言中的字符串可以分配于栈空间 , 堆空间或者只读存储区
字符串长度
? 字符串的长度就是字符串所包含字符的个数
? C 语言中的字符串长度指的是第一个’\ 0 ’ 字符前出现的字符个数
? C 语言中通过’\0’ 结束符来确定字符串的长度
strlen 的返回值是用无符号数定义的 , 因此
相减不可能产生负数 , 以上的语句不等价 。
? 一般情况下 , 千万千万不要自行编写C 标准库已经提供的函数 。
? 标准库有时会使用汇编语言实现 , 目的就是为了充分利用机器所提供的特殊指令以追求最大的速度 。
? 复用已经存在的函数库会更高效 。
不受限制的字符串函数
? 不受限制的字符串函数是通过寻找字符串的结束符
’\0’ 来判断长度
字符串复制: :
char* strcpy(char* dst, const char* src);
字符串连接: :
char* strcat(char* dst, const char* src);
字符串比较 :
int strcmp(const char* s1, const char* s2);
? 不受限制的字符串函数都是以‘ \0 ’ 作为结尾标记来进
行的 , 因此输入参数中必须包含’ \0’ 。
? strcpy 和strcat 必须保证目标字符数组的剩余空间足
以保存整个源字符串 。
? strcmp 以0 值表示两个字符串相等
? 第一个字符串 大于 第二个字符串的时候返回值 大于0
? 第一个字符串 小于 第二个字符串的时候返回值 小于0
? strcmp 不会修改参数值 , 但依然以’\ \0 0’ 作为结束符
长度受限的字符串函数
? 长度受限的字符串函数接收一个显示的长度参数用于
限定操作的字符数
字符串复制: :
char* strncpy(char* dst, const char* src, size_t len);
字符串连接: :
char* strncat(char* dst, const char* src , size_t len);
字符串比较 :
int strncmp(const char* s1, const char*s2 , size_t len);
? strncpy 只复制len 个字符到目标字符串
? 当源字符串的长度小于len 时 , 剩余的空间以’\0’ 填充 。
? 当源字符串的长度大于len 时 , 只有len 个字符会被复制 , 且
它将不会以’\0’ 结束 。
? strncat 最多从源字符串中复制len 个字符到目标串中
? strncat 总是在结果字符串后面添加’\0’
? strncat 不会用’\0’ 填充目标串中的剩余空间
? strncmp 只比较len 个字符是否相等
指针数组和数组指针分析
array 代表数组首元素的地址 , 那么matrix 代表什么 ?
array 和&array 的地址值相同 , 但是意义不同 , 那么指向它们的
指针类型相同吗 ?
? C 语言中通过typedef 为数组类型重命名
typedef type(name)[size];
? 数组类型 :
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
? 数组定义 :
AINT5 iArray;
AFLOAT10 fArray;
数组指针
? 数组指针用于 指向一个数组
? 数组名是数组首元素的起始地址 , 但并不是数组的起始地址
? 通过将取地址符 & 作用于数组名可以得到数组的起始地址
? 可通过数组类型定义数组指针 : ArrayType* pointer;
? 也可以直接定义 :type (*pointer)[n];
? pointer 为数组指针变量名
? type 为指向的数组的类型
指针数组
? 指针数组是一个普通的 数组
? 指针数组中 每个元素为一个指针
? 指针数组的定义 :type* pArray[n];
? type* 为数组中每个元素的类型
? pArray 为数组名
? n 为数组大小
float* a[3]
float*
float*
float*
main 函数的参数
int main()
int main(int argc)
int main(int argc, char *argv[])
int main(int argc, char *argv[], char *env[])
argc – 命令行参数个数
argv – 命令行参数数组
env – 环境变量数组
? main 函数可以理解为操作系统调用的函数
? 在执行程序的时候可以向main 函数传递参数
小结
? 数组指针本质上是一个指针
? 数组指针指向的值是数组的地址
? 指针数组本质上是一个数组
? 指针数组中每个元素的类型是指针
多维数组和多维指针
指向指针的指针
? 指针变量在内存中会占用一定的空间
? 可以定义指针来保存指针变量的地址值
指向指针的指针
? 为什么需要指向指针的指针 ?
? 指针在本质上也是变量
? 对于指针也同样存在传值调用与传址调用
二维数组与二级指针
? 二维数组在内存中以一维的方式排布
? 二维数组中的第一维是一维数组
? 二维数组中的第二维才是具体的值
? 二维数组的数组名可看做常量指针
int a[3][3]
a[0]
a[1]
a[2]
数组名
? 一维数组名代表数组首元素的地址
int a[5] ? a 的类型为int*
? 二维数组名同样代表数组首元素的地址
int m[2][5] ? m 的类型为int(*)[5]
结论 :
. 1. 二维数组名可以看做是指向数组的常量指针
. 2. 二维数组可以看做是一维数组
. 3. 二维数组中的每个元素都是同类型的一维数组
如何动态申请二维数组
以二维指针模拟
小结
C 语言中只有一维数组 , 而且数组大小必须在
编译期就作为常数确定
C 语言中的数组元素可是任何类型的数据 , 即
数组的元素可以是另一个数组
C 语言中只有数组的大小和数组首元素的地址
是编译器直接确定的
数组参数和指针参数分析
退化的意义
C 语言中只会以值拷贝的方式传递参数
? 当向函数传递数组时
? 将整个数组拷贝一份传入函数
? 将数组名看做常量指针传数组首元素地址
C 语言以 高效 为最初设计目标 , 在函数传递的时
候如果拷贝整个数组执行效率将大大下降 。
二维数组参数
? 二维数组参数同样存在退化的问题
? 二维数组可以看做是一维数组
? 二维数组中的每个元素是一维数组
? 二维数组参数中第一维的参数可以省略
void f(int a[5]); ?? void f(int a[]); ?? void f(int* a);
void g(int a[3][3]); ?? void g(int a[][3]); ?? void g(int (*a)[3]);
等价关系
数组的指针 :char (*a)[4] 二维数组 :char a[3][4]
指针的指针 :int** a 指针数组 :int* a[5]
指针 :float* a 一维数组 :float a[5]
等效的指针参数 数组参数
注意事项
C 语言中无法向一个函数传递任意的多维数组
为了提供正确的指针运算 , 必须提供除第一维之外的
所有维长度限制
一维数组参数 – 必须提供一个标示数组结束位置的长度信息
二维数组参数 – 不能直接传递给函数
三维或更多维数组参数 – 无法使用
函数与指针分析
函数类型? C C 语言中的函数有自己特定的类型
? 函数的类型由 返回值 , 参数类型 和 参数个数 共同决定
例 :int add(int i, int j) 的类型为int(int, int)
? C 语言中通过typedef 为函数类型重命名
typedef type name(parameter list)
? 例 :
typedef int f(int, int);
typedef void p(int);
函数指针
? 函数指针用于指向一个函数
? 函数名是执行函数体的入口地址
? 可通过函数类型定义函数指针 : FuncType* pointer;
? 也可以直接定义 :type (*pointer)(parameter list);
? pointer 为函数指针变量名
? type 为指向函数的返回值类型
? parameter list 为指向函数的参数类型列表
回调函数
? 回调函数是利用函数指针实现的一种调用机制? 回调机制原理
? 调用者不知道具体事件发生的时候需要调用的具体函数
? 被调函数不知道何时被调用 , 只知道被调用后需要完成的任务
? 当具体事件发生时 , 调用者通过 函数指针 调用具体函数
? 回调机制的将调用者和被调函数分开 , 两者互不依赖
指针阅读技巧解析
? 右左法则1. 从最里层的圆括号中未定义的标示符看起
2. 首先往右看 , 再往左看
3. 当遇到圆括号或者方括号时可以确定部分类型 ,
并调转方向
4. 重复2,3 步骤 , 直到阅读结束
#include <stdio.h>
int main()
{
int (*p2)(int*, int (*f)(int*));
int (*p3[5])(int*);
int (*(*p4)[5])(int*);
int (*(*p5)(int*))[5];
}
动态内存分配
为什么使用动态内存分配C 语言中的一切操作都是基于内存的
变量和数组都是内存的别名 , 如何分配这些内存由编
译器在编译期间决定
定义数组的时候必须指定数组长度
而数组长度是在编译期就必须决定的
malloc 和free
? malloc 和free 用于执行动态内存分配和释放
? malloc 所分配的是一块连续的内存 , 以字节为单位 ,
并且不带任何的类型信息
? free 用于将动态内存归还系统
void* malloc(size_t size);
void free(void* pointer);
注意 :
• malloc 实际分配的内存可能会比请求的稍微多一点 , 但是不
能依赖于编译器的这个行为
• 当请求的动态内存无法满足时malloc 返回NULL
• 当free 的参数为NULL 时 , 函数直接返回
calloc 和realloc
? 你认识malloc 的兄弟吗 ?
void* calloc(size_t num, size_t size);
void* realloc(void* pointer, size_t new_size);
? calloc 的参数代表所返回内存的类型信息
? calloc 会将返回的内存初始化为0
? realloc 用于修改一个原先已经分配的内存块大小
? 在使用realloc 之后应该使用其返回值
? 当pointer 的第一个参数为NULL 时 , 等价于malloc
小结
? 动态内存分配是 C 语言中的强大功能
? 程序能够在需要的时候有机会使用更多的内存
? malloc 单纯的从系统中申请固定字节大小的内存
? calloc 能以类型大小为单位申请内存并初始化为0
? realloc 用于重置内存大小
程序中的栈
? 栈是现代计算机程序里最为重要的概念之一
? 栈在程序中用于维护函数调用上下文 , 没有栈就没有
函数 , 没有 局部变量
? 栈保存了一个函数调用所需的维护信息
? 函数参数 , 函数返回地址
? 局部变量
? 函数调用上下文
程序中的 堆
? 为什么有了栈还需要堆 ?
? 栈上的数据在函数返回后就会被释放掉 , 无法传递到函数外
部 , 如 : 局部数组
? 堆是程序中一块巨大的内存空间 , 可由程序自由使用
? 堆中被程序申请使用的内存在程序主动释放前将一直有效
堆空间通过申请才能获得 !
? 系统对堆空间的管理方式
? 空闲链表法 , 位图法 , 对象池法等等
程序中的 静态存储区
? 程序静态存储区随着程序的运行而分配空间 , 直到程
序运行结束
? 在程序的 编译期 静态存储区的大小就已经确定
? 程序的静态存储区主要用于保存程序中的 全局变量和
静态变量
? 与栈和堆不同 , 静态存储区的信息最终会保存到可执
行程序中
小结
? 栈 , 堆和静态存储区是C语言程序常涉及的三个基本内存区
? 栈区主要用于 函数调用的使用
? 堆区主要是用于 内存的动态申请和归还
? 静态存储区用于 保存全局变量和静态变量
野指针和内存操作分析
初识野指针
? 野指针通常是因为 指针变量中保存的值不是一个合法的内存地址 而造成的
? 野指针不是NULL 指针 , 是指向不可用内存的指针
? NULL 指针不容易用错 , 因为if 语句很好判断一个指针
是不是NULL
C 语言中没有任何手段可以判断一个指针是否为野指针 !
? 局部指针变量没有被初始化
? 使用已经释放过后的指针
? 指针所指向的变量在指针之前被销毁
非法内存操作分析
? 结构体成员指针未初始化
? 没有为结构体指针分配足够的内存
内存初始化分析
? 内存分配成功 , 但并未初始化
内存越界分析
? 数组越界
内存泄露分析
多次指针释放
使用已释放的指针
? 用malloc 申请了内存之后 , 应该立即检查指针值是否
为NULL , 防止使用值为NULL 的指针
? 牢记数组的长度 , 防止数组越界操作 , 考虑使用柔性
数组
? 动态申请操作必须和释放操作匹配 , 防止内存泄露和
多次释放
? free 指针之后必须立即赋值为NULL
认清函数的真面目
? 函数的由来
序 程序 = 据 数据 + 算法
C 序 程序 = 据 数据 + 函数
函数的意义
? 模块化程序设计
面向过程的程序设计
? 面向过程是一种以过程为中心的编程思想
? 首先将复杂的问题分解为一个个容易解决的问题
? 分解过后的问题可以按照步骤一步步完成
? 函数是面向过程在C 语言中的体现
? 解决问题的每个步骤可以用函数来实现
声明和定义
? 程序中的声明可理解为预先告诉编译器实体的存
在 , 如 : 变量 , 函数 , 等等
? 程序中的定义明确指示编译器实体的意义
声明和定义并不相同 !!!
函数参数
函数参数在本质上与局部变量相同 , 都是在栈上分配空间
? 函数 参数的初始值 是函数 调用时的实参值函数参数的求值顺序依赖于编译器的实现 !!
C 语言中大多数运算符对其操作数求值的顺
的 序都是依赖于编译器的实现的 !!!
int i = f() * g();
程序中的顺序点
? 程序中存在一定的顺序点? 顺序点指的是执行过程中修改变量值的最晚时刻
? 在程序达到顺序点的时候 , 之前所做的一切操作必须反映
到后续的访问中
? 每个完整表达式结束时
? &&, ||, ?:, 以及逗号表达式的每个运算对象计算之后
? 函数调用中对所有实际参数的求值完成之后( 进入函数体之前)
函数的缺省认定
? C 语言会默认没有类型的函数参数为int
小结
? C 语言是一种面向过程的语言
? 函数可理解为解决问题的步骤
? 函数的实参并没有固定的计算次序
? 顺序点是C 语言中变量改变的最晚时机
? 函数定义时参数和返回值的缺省类型为int
函数递归与函数设计技巧
递归概述? 递归是数学领域中概念在程序设计中的应用
? 递归是一种强有力的程序设计方法
? 递归的 本质 为函数内部在适当的时候调用自身
递归函数
?C 递归函数有两个主要的组成部分 :
? 点 递归点 – 以不同参数调用自身
? 出口 – 不在递归调用
f(x) =
1; x = 1
x * f(x-1); x > 1
小结
? C 语言中的递归函数 必然会使用判断语句
? 递归函数在需要编写的时候 定义函数的出口 , 否则
栈会溢出
? 递归函数是一种分而治之的思想
函数设计技巧
? 不要在函数中使用全局变量 , 尽量让函数从意义上是一个
独立的功能模块
? 参数名要能够体现参数的意义
void str_copy (char *str1, char *str2);
void str_copy (char *str_dest, char *str_src);
函数设计技巧
? 如果参数是指针 , 且仅作输入参数用 , 则应在类型前加
const , 以防止该指针在函数体内被意外修改
void str_copy (char *str_dest, const char *str_src);
函数设计技巧
? 不要省略返回值的类型 , 如果函数没有返回值 , 那么
应声明为void 类型
? 在函数体的“ 入口处” , 对参数的有效性进行检查 , 对
指针的检查尤为重要
? 语句不可返回指向“ 栈内存” 的“ 指针” , 因为该内存在
函数体结束时被自动销毁
函数设计技巧
? 函数体的规模要小 , 尽量控制在80 行代码之内
? 相同的输入应当产生相同的输出 , 尽量避免函数带有“ 记
忆” 功能
? 避免函数有太多的参数 , 参数个数尽量控制在4 个以内
函数设计技巧
? 有时候函数不需要返回值 , 但为了增加灵活性 , 如支持链式表达 , 可
以附加返回值
char s[64];
int len = strlen(strcpy(s, “Android”));
函数名与返回值类型在语义上不可冲突
char c;
c = getchar();
if(EOF == c)
{
//…
}