C语言——指针(第六讲)(全)
前言
亲爱的CSDN的小伙伴们,我是陪伴你们学编程的莹莹同学。今天,我给大家分享的是指针,相信大家就算没有学过指针,也应该听说过它吧!没错,尽管指针听起来让人望而生畏,但是相信大家看过这篇文章,可以对你们有所帮助,好了,话不多说去,我们开始吧!
指针引言
我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
我们把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。而一个比特位可以存储⼀个2进制的位1或者0。
其中,每个内存单元,相当于⼀个学⽣宿舍,⼀个⼈字节空间⾥⾯能放8个⽐特位,就好⽐同学们住的⼋⼈间,每个⼈是⼀个⽐特位
每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间
⽣活中我们把⻔牌号也叫地址,在计算机中我们
把内存单元的编号也称为地址。C语⾔中给地址起
了新的名字叫:指针。
结论:内存单元的编号=地址=指针
补充: 1byte = 8bit;
1KB = 1024byte;
指针变量,取地址操作符,解引用操作符
取地址操作符
在C语⾔中创建变量其实就是向内存申请空间,比如int a = 10;
上述的代码就是创建了整型变量a,内存中申请4个字节。
而使用取地址操作符”&“便可以取出a的地址。
#include<stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
int* p = &a;//指针变量
return 0;
}
指针变量
我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
答案是:指针变量中。(如上代码)
注:指针变量也是变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
拆解指针类型
对于int* p = &a;
我们看到p的类型是 int*,* 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。
指针变量的⼤⼩
指针变量的⼤⼩取决于地址的⼤⼩
32位平台下(x86环境)地址是32个bit位(即4个字节)
64位平台下(x64环境)地址是64个bit位(即8个字节)
注意:指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
字符指针变量
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;//pc是指针变量
char arr[10] = "abcdef";//数组字符串,可以被修改
char* p1 = arr;
*p1 = 'w';
const char* p2 = "abcdef";//常量字符串,不能被修改;
//打印
printf("%s\n", p1);
return 0;
}
数组指针变量, 指针数组
指针数组,顾名思义是存放指针的数组。
指针数组的每个元素都是⽤来存放地址(指针)的。
数组指针变量
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
int *p1[10];//p1是指针数组
int (*p2)[10];//p2是指针变量
int (*p)[10];
解释:p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
函数指针变量
函数指针变量是用来存放函数的地址的
#include<stdio.h>
int main()
{
return 0;
}
#include<stdio.h>
void Add(int x, int y)
{
return x + y;
}
char* test(char c, int n)//此函数的返回类型是char*
{
//
}
int main()
{
printf("%p\n", &Add);
int (*pf)(int,int) = &Add;//pf是专门用来存放函数的地址,pf就是函数指针变量
char* (*p)(char, int) = &test;
return 0;
}
函数指针变量的使用
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
int c = Add(2, 3);//函数名调用
printf("%d\n", c);
int d = (*pf)(3, 4);//函数指针调用
printf("%d\n", d);
int d = pf(3, 4);//*可以不写
printf("%d\n", d);
return 0;
}
函数指针变量类型
解引⽤操作符
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
*p;//解引用操作符(间接访问操作符)
printf("%d\n", *p);
return 0;
}
*pa 的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以pa=0,这个操作符是把a改成了0.
意义:其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,后期慢慢就能理解了。
指针变量类型的意义
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//调试:代码1会将n的4个字节全部改为0
//代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
//调试:代码2只是将n的第⼀个字节改为0
以上:char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节
结论:指针的类型决定了,对指针解引⽤的时候访问几个字节。
拓展:
指针类型 | 指向 | 解引用访问字节数 |
---|---|---|
char* | 指向字符的指针 | 解引用访问1个字节数 |
short* | 指向短整型的指针 | 解引用访问2个字节数 |
int* | 指向整型的指针 | 解引用访问4个字节数 |
float* | 指向单精度浮点型的指针 | 解引用访问4个字节数 |
指针±整数
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("pa=%p\n", pa);
printf("pa+1=%p\n", pa+1);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
return 0;
}
调试后,我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
void*指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void 类型的指针不能直接进⾏指针的±整数和解引⽤的运算。*
#include<stdio.h>
int main()
{
int a = 0;
//float f= 0.0f;
void* p = &a;//int*
//p = &f;//float*
//*p = 10;//无法解引用
return 0;
}
优点:⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。
const修饰指针
const可以修饰变量,使变量加上⼀些限制,不能被修改。
#include<stdio.h>
int main()
{
const int a = 10;//a具有了常属性(不能被修改了)
//a是不是常量呢?
//虽然a是不能被修改的,但本质上还是变量
int* p = &a;
*p = 20;
printf("%d\n", a);
return 0;
}
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
//const放在*的右边
int* const p = &a;
//p = &b;
*p = 100;
return 0;
}
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
//const放在*的左边
int const* p = &a;
p = &b;
return 0;
}
经调试后,得出结论:
const修饰指针变量的时候
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
指针运算
指针的基本运算有三种,分别是:
• 指针±整数
• 指针-指针
• 指针的关系运算
• 指针±整数
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[i];
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
//或者
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[i];
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
指针-指针
前提条件:两个指针同时指向同一空间
|指针 - 指针| = 两个指针之间的元素个数
#include<stdio.h>
int main()
{
int a[10] = { 0 };
printf("%d\n", &a[9] - &a[0]);
printf("%d\n", &a[0] - &a[9]);
return 0;
}
指针运算关系
本质:指针和指针比较大小
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的原因
- 指针未初始化
#include<stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
- 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
*p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
- 指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;//在p指向n的同时,n已经返还给空间了
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
如何规避野指针
1.指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
2.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
assert断⾔
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏**。这个宏常常被称为“断⾔”。**
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
优点:assert出现错误的时候,直接会报错,指明在文件什么位置,哪一行,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
指针的使⽤和传址调⽤
写一个函数,交换两个整数的值
//Swap
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
编译后发现a,b值并没有发生交换
因为实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
所以Swap是失败的了。
//Swap 2
#include<stdio.h>
void sweep(int* x, int* y)
{
int typ;
typ = *x;
*x = *y;
*y = typ;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n", a, b);
sweep(&a,&b);
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}
编译发现,交换成功
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。
数组名结论
1.数组名就是数组⾸元素(第⼀个元素)的地 址。 2.但两个例外: • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节 • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的) 除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
//或者
int* p2 = arr;//数组名是数组首个元素的地址
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
使⽤指针访问数组
使用指针来访问数组,实现数组的输入输出
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int* p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
}
for (i = 0; i < sz; i++)
{
printf("%d", *(p + i));//*(p+i) = p[i]或者arr[i] = *(arr + 1);
}
return 0;
}
函数实现数组的打印
#include<stdio.h>
void Print(int* p,int sz)//一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
Print(arr, sz);
return 0;
}
数组传参的本质
1.对于一维数组:
数组传参本质上传递的是数组⾸元素的地址。
2.对于二维数组:
二维数组传参的本质也是传递了地址,传递的是第一行这个一维数组的地址。
二级指针
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;//p是一级指针
int** pp = &p;//pp是二级指针
//打印p的地址
printf("%p\n", *pp);
//**pp先通过*pp找到p,然后对p进行解引用操作:*p,那找到的是a
printf("%d\n",* *pp);
return 0;
}
函数指针数组
#include<stdio.h>
void Add(int x, int y)
{
return x + y;
}
void Sub(int x, int y)
{
return x - y;
}
void Mul(int x, int y)
{
return x * y;
}
void Div(int x, int y)
{
return x / y;
}
int main()
{
int(*pf1)(int, int) = Add;//函数指针变量
int(*pfarr[4])(int, int) = { Add,Sub,Mul,Div };//pfar是函数指针数组
int i =0;
for(i = 0;i < 4;i++)
{
int r = pfarr[i](8,4);
printf("%d\n",r);
}
return 0;
}
typedef关键字
typedef是⽤来类型重命名的,可以将复杂的类型,简单化
你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:
typedef unsigned int uint;
//将unsigned int 重命名为uint
如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:
typedef int* ptr_t;
但是对于数组指针和函数指针稍微有点区别:
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
qsort函数
介绍
void qsort(void * base, //指针,指向的是待排序的数组的第一个元素
size_t num, //base指向的待排序数组的元素个数
size_t size, //base指向的待排序数组的元素大小
int(*compar)(const void*, const void*))//函数指针——指向的是两个元素的比较函数
写一段代码使用qsort排序整型数据
#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* p1, const void* p2)
{
/*if (*(int*)p1 > *(int*)p2)
return 1;
else if (*(int*)p1 > *(int*)p2)
return 0;
else
return -1;*/
//简化
return (*(int*)p1 - *(int*)p2);
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void test1()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr,sz ,sizeof(arr[0]),cmp_int );
print_arr(arr, sz);
}
int main()
{
test1();
return 0;
}
使用qsort排序结构体数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void test2()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}
int main()
{
test2();
return 0;
}
qsort函数的模拟实现
#include<stdio.h>
void bubble_sort(int arr[], int sz) //局限性:这个函数只能排序整型数组
{
int i = 0;
for (i = 0; i < sz -1; i++)
{
int j = 0;
for (j = 0; j < sz -1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int t = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = t;
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print_arr(arr, sz);
return 0;
}
sizeof和strlen对比
sizeof | strlen |
---|---|
1. sizeof是操作符 | 1. strlen是库函数,使⽤需要包含头⽂件 string.h |
2. sizeof计算操作数所占内存的⼤⼩,单位是字节 | 2. srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数 |
3. 不关注内存中存放什么数据 | 3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能会越界 |
(完) |