Files
BlogPosts/Collection/YoudaoyunNotes/02C语言/08-指针.md

410 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# **1. 准备知识**
**1.1 内存地址**
- 字节:字节是内存的容量单位,英文称为 Byte一个字节有8比特位即 1Byte = 8bits 1B = 8b
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
![](images/WEBRESOURCE73f0b54259e8460e97f84fd70e8aaef4stickPicture.png)
## **1.2 基地址**
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节地址中编号最小的那个,称为基地址。
![](images/WEBRESOURCE06e4a4aac18e468d89b657886f10a5b3stickPicture.png)
### **1.3 取址符**
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址
- 注意:&符号的左边有操作数则表示位与运算符,若左边没有操作数则表示取地址符
```c
int main(int argc, char *argv[])
{
    int a=110;
    printf("整型变量a的地址是%p\n", &a);
    char b='a';
    printf("字符型变量b的地址是%p\n", &b);
    float c=3.14;
    printf("浮点型变量c的地址是%p\n", &c);
    printf("整型变量a的地址长度是  %ld\n", sizeof(&a));
    printf("字符型变量b的地址长度是%ld\n", sizeof(&b));
    printf("浮点型变量c的地址长度是%ld\n", sizeof(&c));
    return 0;
}
```
- 总结
- 虽然不同类型的变量的内存尺寸不同,但是他们的地址尺寸(地址编号的位数)却是相同的(地址的尺寸与系统的字长相关32位系统地址尺寸为32位64位系统地址尺寸为64位)
- 不同的地址虽然表示形式一样,但他们所代表的内存尺寸(在内存中所占内存大小)和类型都不相同,因此相同形式的地址编号在逻辑上要严格进行区分
![](images/WEBRESOURCEc93ef0073e59a9197d1df93e15a42da9image.png)
# 2.指针入门
## 2.1指针概念
由于翻译问题及口语表达的习惯,在日常表述中,指针会有以下两种含义
- 指地址
```c
int a;
&a; // 我们可以说&a指向a的地址
```
- 指指针变量
```c
int *p; // 可以用于指向int类型数据的地址
```
- *号左右两边都有操作数,表示两数相乘(a*b)
- *号左右两边都有操作数,且左操作数数一个数据类型表示定义一个指针变量(int *p)
- *号左边没有操作数,表示取值操作(*p)
## 2.2指针的定义
- 用于存放数据类型变量的地址的变量称为指针
```c
int *p1; // 用于存储int类型数据的地址p1被称为int型指针或整型指针
char *p2; // 用于存储char类型数据的地址p2被称为char型指针或字符型指针
double *p3; // 用于存储double类型数据的地址p3被称为double型指针或浮点型指针
```
对于int *p;在32位系统下的理解如下
![](images/WEBRESOURCE2f532ae1a2635b8f6d6eb26f2b49be3eimage.png)
![](images/WEBRESOURCE970015c2b46c95da2d92fad50ded8cadimage.png)
- 注意:指针用于存放地址的变量,由于在相同的系统位数下地址的尺寸相同,故指针的尺寸相同
![](images/WEBRESOURCEe6d0d1a5088db204eab4343b8bff4ef4image.png)
- 指针变量的内存尺寸只于系统字长相关,与指针的类型无关。
## 2.3指针的赋值
- 可将一个地址类型于指针类型相同的地址赋值这个指针
```c
int a=100;
char b='A';
double c=3.14;
p1 = &a; // 将a的地址赋值给指针p1它们类型必须相同
p2 = &b; // 将b的地址赋值给指针p2它们类型必须相同
p3 = &c; // 将c的地址赋值给指针p4它们类型必须相同
int *p4 = p1; // 使用整型指针变量p1值初始化赋值整型指针p4
int *p5;
p5 = p1; // 整型指针变量p1值赋值给整型指针p5
int *p6 = &a; // 使用a的地址初始化赋值整型指针变量p6
```
## 2.4指针的索引
- 所谓索引,指的是通过指针变量取得其指向的目标(访问指针变量中所存储的地址中的数据)
```c
*p1 = 200;
*p2 = 'B';
*p3 = 6.62;
a = 10;
a = 10;
```
![](images/WEBRESOURCEe2db6c7a73c7bc9d780f9639545ccf3eimage.png)
## 练习
编写一个函数实现两个数交换。
# 3.特殊指针
## 3.1野指针
- 指向一块未知区域的指针,称为野指针。**野指针是危险的**
![](images/WEBRESOURCE6a63aa7276f21b9af08cd146f5959cbbimage.png)
- 危害
- 引用野指针相当于访问了非法内存常常会导致段错误segmentatio fault
- 引用野指针,可能会破坏系统关键数据,导致系统崩溃等严重后果。
- 产生原因
- 指针定义式未进行初始化
- 指针指向内存被系统回收
- 指针越界
- 如何防止
- 定义指针时进行及时初始化定义时无法明确指向则赋值为NULL
- 绝不引用已被回收的内存回收内存后令指针指向NULL
- 确认所申请的内存边界,谨防越界
## 3.2空指针
```c
NULL // (void *)0;
```
- 很多情况下,我们不可避免的会遇到野指针,如刚刚定义指针无法立即为其分配一块内存时,又或者指针所指向的内存被释放了等等。
- 对于一个暂时无法让其指向一块合法内存的指针而言, 我们最好将其初始化为“空指针” 即给他赋一个空值(零) 让他指向零地址
![](images/WEBRESOURCE698f0b5e2b11e6c3a965743dd01df4caimage.png)
```c
// 刚刚定义指针无法明确其指向,让其指向零地址以保证安全
int *p = NULL;
char *q = NULL;
//指针指向的内存被释放,令指针立即指向零地址保证安全
char *k = malloc(2); // 给指针分配内存
free(k); // 释放内存
k = NULL;
```
## 3.3void指针
- 概念所谓void指针也称为万能指针(泛型指针)无法明确指针所指向的目标的数据类型则可将这种指针定义为void类型
- 要点:
- void型指针无法索引目标必须将其转换为一个具体的类型指针方可索引目标。
- void型指针不支持加减运算。
> void只用于三种情况
> 定义指针
> 定义函数类型
> 表示函数的参数列表
```c
void swap(void *a, void *b)
{
    // int *p = a;
    // int *q = b;
    // *p^=*q;
    // *q^=*p;
    // *p^=*q;
    *((int *)a) ^= *((int *)b);
    *((int *)b) ^= *((int *)a);
    *((int *)a) ^= *((int *)b);
}
```
## 3.4char型指针
- char型指针实质上与别的类型指针并无本质区别但由于C语言中字符串以字符数组的方式进行存储而数组在大多数场合又表现为指针因此字符串在绝大多数场合下表现为char型指针
```c
char *p = "abcd"; // p指向的目标是字符串常量的地址
*p = 'x'; // 不可以修改常量数据
/* 上面的写法语法上无错,但逻辑不允许,推荐下面的写法,编译器会帮助我们检查语法错误 */
const char *q = "abcd"; // q指向的目标是字符串常量的地址
*q = 'x';
```
## 3.5const型指针
- const(只读)型指针有两种形式:①常指针(指针只读)②常目标指针(目标只读)
- 常指针const修饰指针本身表示指针变量本身无法修改(指针常量)
```c
char * const p;
```
![](images/WEBRESOURCE55871207d1eef11e0fd010e335e4cc0f576913ed193ece92e9ad7fc8a81dc9cf.png)
```c
char a,b;
char  * const p = &a;
// p = &b; // 错误,常指针不能修改其值
*p = 'S'; // 可通过指针对目标进行读写
printf("%c\n", *p);
```
- 常目标指针const修饰指针的目标表示无法通过该指针修改其目标常量指针
![](images/WEBRESOURCE51fc80bb52e549d39a1753ac77f656e5stickPicture.png)
```c
const char  *q = &a;
char const  *k = &a; // k和q等价
q = &b; // 指针指向的目标可以修改
k = &b;
*q = 'a'; // 错误,常目标指针无法写访问目标
printf("%c\n", *k); // 正确。常目标指针读可访问目标
```
- 常指针在实际应用中不常见。
- 常目标指针在实际应用中广泛可见,用来限制指针的读写权限
## 晚上作业
[指针与数组作业.doc](attachments/WEBRESOURCEd71090e9d6659f19f9b36c93099793f4指针与数组作业.doc)
## 3.6函数指针
- 指向函数的指针称为函数指针
- 函数指针与普通指针本质上并无区别,只是在取址和索引时取址符&与索引符*均可省略
```c
 double fun(double a[], int len)
{
   double max = a[0];
   for(int i=0; i<len; i++)
   {
        max = max>a[i]?max:a[i];
   }
   double min = a[0];
   for(int i=0; i<len; i++)
   {
        min = min<a[i]?min:a[i];
   }
   return max-min;
}
int main(int argc, char *argv[])
{
     double num[] = {1.23, 3.21,1.010,2.63,8.52};
     double (*p)(double [], int ) = &fun;  // p是一个函数指针指向一个返回值为double形参为一个double型地址与一个int型数据的函数
     printf("%02f\n", (*p)(num, 5));  // 通过指针p访问目标
     printf("%02f\n", fun(num, 5));
     /* C语言中函数的名字代表函数的地址 */
     double (*q)(double *, int) = fun;  
     printf("%02f\n", q(num, 5));  
     printf("%02f\n", fun(num, 5));
    return 0;
}
```
- 注意函数fun == &fun 数组array != &array
- 要点
- 函数指针是一类专门指向某种类型函数的指针
- 函数的类型(返回值与参数列表)不同,所需要的函数指针也不同(指针的类型由函数的返回值与参数列表共同构成)。
- 函数的类型判定方法是在函数声明式中去除掉用户标识符名称,剩下的既是函数类型
## 3.7多级指针
- 若一个指针变量p1存储的地址是普通变量a的地址则p1就称为一级指针
- 若一个指针变量p2存储的地址是指针变量p1的地址则p2就称为二级指针
- 以此类推p2就被称为多级指针
```c
int a;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
```
# 4.指针的运算
- 指针的运算:加与减(+、-、++、--
- 两个指针运算中'+'无意义,两个指针相加没有实际意义;两个指针做减法运算,结果可以得到两个地址间的偏移量(步长)
```c
int a,b;
int *p = &a;
int *q = &b;
p+q; // 得到一个未知的新地址并无意义
p-q; // 得到两个地址间的偏移量即从a的地址到b的地址间的距离步长
```
- 单指针的加法指的是指针自增运算或加上某个数值,表示指针指向的目标地址向上移动(向内存高地址移动)若干步长(步长与种指针的类型相关,一步为数据类型的内存尺寸)
- 单指针的减法指的是指针自减运算或减去某个数值,表示指针指向的目标地址向下移动(向内存低地址移动)若干步长(步长与种指针的类型相关,一步为数据类型的内存尺寸)
```c
int a = 100;
int *p = &a; // 指针p指向a地址
int *k1 = p+2; // 向上移动2个步长
int *k2 = p-3; // 向下移动3个步长
```
![](images/WEBRESOURCE020956fc6459842a83ae6c650bf7e422image.png)
- 要点数据类型不同步长也不同char *p; p++与int *q; q++; 中p移动一步等于内存的一个字节而q移动一步等于内存的四字节。
```c
int array[10] = {1,2,250,4,270,6,7,8,9,10};
int *p = array;
for (int i = 0; i < 10; i++)
{
    printf("%d\t", *p);
    p++; // 每次移动一步长4字节
}
printf("\n");
char *q=(char *)array;
for (int i = 0; i < 10; i++)
{
    printf("%d\t", *(int*)q);
    q+=4; // 每次移动一步长1字节
}
printf("\n");
```
## 练习
- 编写代码通过指针实现将两个字符串进行拼接(不要使用系统库函数)
-char s1[20] = "hello"; char s2[20] = "world";调用自己写的函数后得到一个新的字符串"helloworld"
```c
```
- 编写函数实现将将整型数组中重复的数字进行剔除,剔除后数组的元素个数要发生变化。
-int array[] = {1,2,3,3,4,3,4,5,5};调用函数后变为{1,2,3,4,5};
```c
```