目录
hello大家好,今天我们来讲一下C语言中指针的知识点,指针这部分内容很多,容易记混而且有时候写代码也想不到,这就需要有一个良好的指针基础,我尽全力把指针用我们已经学习过的知识来讲述,让学习者都可以很快的理解,并且可以使用我所讲指针给出的例子来练习。
那么我们就开始学习吧!!!
一、内存和地址
1、生活中的例子
学习指针我们就要知道内存和地址的关系。
下面图中,我们可以把这一栋楼看做内存,每个房间可以看做地址。
如果我们想去到楼里的特定房间,我们就要挨个房间寻找我们想要去的特定房间里面,然后我们再去到另外一个特定的房间里面,我们还需要挨个寻找,这样效率会非常慢。
聪明的同学已经开始想到了,我们可以把每个房间都设置门牌号,这样我们就可以快速去到我们想去的房间了。
结论:门牌号=地址=指针
2、内存的关系
我们可以把每把内存给划分为内存单元,一个内存单元大小等于一个字节,所以每个房间就可以看着是字节。
常见的存储单位:bit(比特)-> Byte(字节) -> KB -> MB -> GB -> TB -> PB
1字节 = 8bit位
二、指针变量和地址
1、&符号,%p占位符
取地址符号(&):可以让我们获取地址。
%p:想打印出地址就需要使用%p这个占位符。
int main( )
{
int a = 10;
printf("%p\n", &a);
return 0;
}
输出:
2、一个简单的指针代码。
int main()
{
int a = 10;
int* p = &a;
return 0;
}
通过调试vs我们看见p=&a。
在这段代码中,我们设置了a=10,我们通过*p指向了a的地址,此时p就等于a的地址。我们就可以通过*p来修改a中的地址。
3、理解指针
int a = 10;
int * pa = &a;
pa左边写的是int*,*说明是pa的指针变量,而int在说明pa指向的是一个整型类型的对象。
4、解引用操作符
解引用操作符就是" * "。
int main()
{
int a = 10;
int* p = &a;
*p = 20;
printf("%d ", *p); //输出20
return 0;
}
这段代码中,我们通过*p=20;修改a的数值。尽管int* p = &a;这一行代码我们已经获取了a的地址,但是我们还是需要通过操作符" * "来访问或修改这样地址指向的值。p中存放的内容是a的地址,如果我们直接写p=20,程序修会报错,因为p是一个指针,不能直接存放一个整数。只有我们使用*p,我们才能直接访问p指向的内存空间,即a的内存空间,从而修改变量中的a,此时a的数值也会跟着改变。
5、指针变量的大小。
指针变量的大小是取决于我们使用的是32位平台还是64位平台。
int main()
{
printf("%d ", sizeof(char*));
printf("%d ", sizeof(int*));
printf("%d ", sizeof(double*));
printf("%d ", sizeof(float*));
return 0;
}
32位平台输出:
64位平台输出:
在32位下,指针的大小位4个字节。
在64位下,指针的大小位8个字节。
三、指针变量类型的意义
1、指针解引用的作用
int main()
{
int a = 0x11223344;
char* p = (char*)&a;
*p = 0;
return 0;
}
*p运行前&a地址:
*p运行后&a地址:
为什么只有一个字节等于0,因为*p是一个char类型,char类型只占一个字节。
只有*p跟a是一个类型的时候,才可以把a=10。
2、指针+指针
int main()
{
int a = 10;
char* p1 = (char*)&a;
int* p2 = &a;
printf("%p\n", &a);
printf("%p\n", p1);
printf("%p\n", p1+1);
printf("%p\n", p2);
printf("%p\n", p2+1);
return 0;
}
输出:
我们可以看到,p1指针是char类型,p2是int类型,当把指针+1的时候,p1和p2所得到的地址是不一样的,p1加1字节,p2加4字节,这是因为指针的类型是不一样的。
结论:指针的类型决定的指针向前的步子是多大。
3、指针-指针
int main()
{
int a = 10;
char* p1 = (char*)&a;
int* p2 = &a;
printf("%p\n", &a);
printf("%p\n", p1);
printf("%p\n", p1 - 1);
printf("%p\n", p2);
printf("%p\n", p2 - 1);
return 0;
}
输出:
4、void*指针
int main()
{
int a = 10;
char* p = &a;
*p = 20;
return 0;
}
char* p = &a;由于我们使用char类型接收int类型,导致编译器提示错误。
void*可以接收任意指针,char,int,long,double等等
int main()
{
int a = 10;
void* p = &a;
return 0;
}
但是void*是无法修改指针变量的
int main()
{
int a = 10;
int b = 20;
void* p1 = &a;
void* p2 = &b;
*p1 = 30;
*p2 = 40;
return 0;
}
那么void*类型的指针有什么用呢?
void*是使用函数参数部分,用来接收任意不同类型的数据,可以实现泛编程效果,使得一个函数可以出来多个数据类型。
四、const修饰指针
1、const修饰变量
const这个函数可以让变量中避免被修改。
int main()
{
const int a = 10;
a = 20;
return 0;
}
但是我们仍然可以使用指针来修改a变量
int main()
{
const int a = 10;
int* p = &a;
*p = 20;
printf("%d", *p);
return 0;
}
输出:
那么就有人问了,这有什么意思,这不就是防君子不防小人吗,那么我们如果才可以防止a被修改呢?
2、const修饰指针变量
这些有什么区别呢?
int* p; //没有const
const int* p; //const在*左边
int const* p; //const在*左边
int* const p; //const在*右边
int const * const p; //const在*两边
1、没有const
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;
p = &m;
}
在没有const限制的条件下我们可以修改指针*p的变量,p也可以获得m的地址
2、const在*左边
const在*左边有两种写法,这两种写法都是正确的:
const int* p =&a;
int const* p =&a;
void test1()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
在const限制的条件下我们不可以修改指针*p的变量,但是p可以获得m的地址.
3、const在*右边
void test1()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;
}
const限制的条件下我们可以修改指针p的数值,但是p不可以获得m的地址。
4、const在*两边
void test1()
{
int n = 10;
int m = 20;
int const* const p = &n;
*p = 20;
p = &m;
}
const限制的条件下我们不可以修改指针p的数值,p也不可以获得m的地址。
总结:
- const在左边的时候,修饰的是指针指向的内容,保证指向内容不能被指针修改,但是指针变量本身的内容是可变的。
- const在右边的时候,修改是是指针变量本身,保证指针指向的内容是可以被修改的,但是指针变量本身是无法被修改的
五、指针运算
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
下图是数组中每一个元素的下标
1、指针+-整数
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//指针+整数
}
return 0;
}
指针*p的类型是int,地址是&arr[0],下标是0,通过p+i,我们就可以打印出指针指向每一个元素下标地址,通过解引用我们从而获得打印数组。
我们之前学习的是printf("%d ", arr[i]);这样打印出数组,但是在编译器底层是通过指针来打印的,*(p + i)= arr[i]。
2、指针-指针
#include <stdio.h>
int my_strlen(char* s)
{
char* p = s;
int count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
printf("%d\n", my_strlen("abcdef"));
return 0;
}
输出字符串长度:
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s; //当p结束是指向字符串的末尾,所以我们用末尾-初始值就是字符串长度
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
3、指针的关系运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < &arr[sz]) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
while (p < &arr[sz])这一行代码,我们是利用地址来进行比较的,当p这个地址小于&arr[sz]地址的时候,那么就不允许。
六、野指针
指针指向实际方向的内容是否有效,超出指针指向变量部分的内容,或者是栈帧销毁的内容会成为野指针,野指针的数值是一个随机值。
1、野指针的成因
1、未初始化
int main()
{
int* p1;
*p = 10;
return 0;
}
2、越界访问
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = &arr[0];
for (int i = 0; i < 11; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
输出:
在这个一维数组打印中,指针超出了指向数组的值,造成了随机值,这就是野指针。
3、指针指向空间的释放
int test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d", *p);
return 0;
}
在这一段代码中*p也是一个野指针,这是为什么,因为我们创建的test函数在运行完以后就会销毁,所以*p指向的是&n,test函数已经被销毁了,*p就没有地址了,导致*p变成了野指针。
2、如何避免野指针
int main()
{
int a = 10;
int* p1 = &a;
int* p2 = NULL;
return 0;
}
我们可以在创建的int* p里放入NULL,就可以避免野指针出现。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = &arr[0];
for (int i = 0; i < 11; i++)
{
*(p++) = i;
}
//p已经越界,把NULL赋值给p
p = NULL; //把野指针的数值等于NULL
p = &arr[0]; //重新赋值p
if (p!=NULL)
{
printf("haha\n");
}
return 0;
}
七、assert断⾔
assert头文件:
assert (p!=NULL);
assert是一个对程序员很友好的功能,如果assert这个程序符合调价,那么就继续运行,如果不符合那就会提示错误在哪些地方。
如果代码没有任何问题以后,可以在头文件上面写:
#define NDEBUG
#include <assert.h>
int main()
{
int age = 11;
// 使用assert检查年龄是否大于18岁
assert(age >= 18);
printf("年龄大于18岁。\n");
return 0;
}
八、指针的使⽤和传址调⽤
1、strlen的模拟实现
方法一:
int my_strlen(const char* str)
{
int count = 0;
assert(str);
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len= my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
方法二:
int my_strlen(const char* str)
{
char* p = str;
assert(str);
while (*p)
{
p++;
}
return p-str;
}
int main()
{
int len= my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
输出:
2、传值调用传值调用
1、传值调用
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
为什么明明在函数中交换了a和b的数值,为什么没有交换成功?
2、传址调用
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
输出:
指针并不是跟形参一样是拷贝,指针指向的数据就是a,b这两个地址,当函数Swap2中px,py中数据进行交换的时候,交换的就是main函数中a,b中真实的数据。