文章目录
c语言指针(一)
1.指针是什么?
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量
那我们就可以这样理解:
内存:
指针变量:
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量.
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'a';
return 0;
}
从调试的监视窗口可以发现,pc为指针变量,指向变量ch的地址,所以变量pc和ch的地址是一样的!!!
总结: 指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是: 一个小的单元到底是多大?(1个字节)如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);那么32根地址线产生的地址就会是
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。每个地址标识一个字节,那我们就可以给4G的空间进行编址(232Byte==232/1024KB==232/1024/1024MB==232/1024/1024/1024GB == 4GB)。
同样的方法,那64位机器,如果给64根地址线,把上述公式32换成64位即可计算得出。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
1.1 解引用操作符
我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,pa其实就是a变量了;所以pa = 0,这个操作符是把a改成了0.那么这⾥如果⽬的就是把a改成0的话,写成 a = 0; 不就完了,为啥⾮要使⽤指针呢? ---------->>
其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,后期慢慢就能理解了。
2. 指针和指针类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。譬如如下代码:
#include <stdio.h>
int main()
{
int num = 10;
int *pi = #
printf("&num = %p\n", &num);
printf("&*pi = %p\n", &*pi);
return 0;
}
此代码将&num(num的地址)保存到pi中,发现通过pi指针指向num地址,便可以让两者地址相同。
2.1指针±整数
#include <stdio.h>
int main()
{
int a = 10;
char* pc = (char*)&a;
int* pi = &a;
printf("%p\n", &a);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);
return 0;
}
从结果可以发现:我们知道char表示一个字节,int是4个字节,在pc指向char类型的a地址时,加一位便是加1个字节,而pi指针指向的是int型a地址,所以当pi指针加1时,就是加一个int类型大小(4个字节)。
总结:
1.char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
2.指针的类型决定了指针向前或者向后走一步有多大(距离).
2.2指针的解引用
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
我们通过逐步调试观察内存的变化:
因为指针pc为char类型,所以在解引用时改变了1个字节,而int型pi指针解引用改变了4个字节
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
2.3 void*指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的±整数和解引⽤的运算。
例如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。⽽使用void类型就不会有这样的问题。
3.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
3.1野指针成因
- 指针未初始化(局部变量指针未初始化,默认为随机值)
#include <stdio.h>
int main()
{
int* p;
*p = 10;
return 0;
}
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
*(p++) = i; 当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
3.指针指向的空间释放
创建了指针变量指向了某个空间,但在调用结束后没有释放指针或将指针初始化。
#include <stdio.h>
int* func()
{
int n = 100;
return &n;
}
int main()
{
int* p = func();
printf("%d\n", *p);
return 0;
}
3.2如何规避野指针
1.指针初始化
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
2.小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
3.指针指向空间释放,及时置NULL,并在指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
for (int i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if (p != NULL) //判断
{
//...
}
return 0;
}
4.避免返回局部变量的地址
与造成野指针空间释放问题一致。
4.指针运算
- 指针± 整数
- 指针-指针
- 指针的关系运算
4.1指针±整数
#include <stdio.h>
#define aaa 5
int main()
{
float val[aaa];
float* vp;
for (vp = &val[0]; vp < &val[aaa];)
{
*vp++ = 0;
}
return 0;
}
在调试过程中观察内存的变化可以发现,每运行一次for循环,vp指针便将当前位置的val数组初始化为了0.
4.2指针-指针
#include <stdio.h>
int func(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main()
{
func("hello solity");
return 0;
}
在调试过程中观察p指针的变化即可!!!这里就不在一一举例查看!!
4.3指针的关系运算
这里将4.1中的for循环代码作为例子:
for (vp = &val[0]; vp < &val[aaa];)
{
*vp++ = 0;
}
改为标准的for循环:
for (vp = &val[aaa-1]; vp >= &val[0]; vp--)
{
*vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
5.指针和数组
我们看一个例子:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5};
printf("&arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
return 0;
}
可见数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址
那么这样写代码是可行的:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = arr; //p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。例如:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p\t", i, &arr[i]);
printf("p + %d = %p ",i, p + i);
printf("\n");
}
return 0;
}
从运行结果可以发现, **p+i **其实计算的是数组 **arr **下标为 i 的地址.
那我们就可以直接通过指针来访问数组:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
6.二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? ------->> 这就是二级指针。
对于二级指针的运算有:
-
*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa
int b = 20; *ppa = &b; //等价于 pa = &b;
-
**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a
**ppa = 30; //等价于*pa = 30; //等价于a = 30;
7.指针数组
指针数组是指针还是数组? 答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。那指针数组是怎样的?
int* arr3[5]; //是什么?
arr3是一个数组,有五个元素,每个元素是一个整形指针。
这里举例说明指针数组的应用:
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[] = { arr1,arr2,arr3 }; //模拟,可看为二维数组
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 5; ++j)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
从该段程序可以看出,指针数组顾名思义就是存放指针的数组!!!
8. assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率