一、在C语言里地址
在C语言里,地址指的是内存中某个位置的标识符(它的名字)。每个变量或数据都有唯一的位置,在这个位置给他取了个名字。我们就可以通过这个名字在内存里找到这个变量或数据。
地址在内存里的形式是十六进制。
int a = 10;
int* p = &a;
二、什么是指针
指针就是地址,地址就是指针,我们使用指针就是在操控地址,通过指针访问地址。
对一个变量使用取地址操作符(&)就可以获得它在内存里的地址(名字),把这个地址(名字)给另一个变量保存起来,它就是指针变量。在指针变量里,星号(*)用来说明这个变量是指针,即变量p是一个指针变量,int 类型说明这个指针变量指向的变量类型是整形,int* p = &a;就完成了对地址进行保存的操作。
怎么使用?
我们已经会将一个变量的地址取出给指针变量保存,下一步就是去使用它,通过解引用操作符(*)完成对地址的操作。
int main()
{
int a = 10;
int* p = &a;
*p = 6;
return 0;
}
代码的逻辑
在int* p里不需要关心*号是贴在int后面,还是变量名p的左边,它们的实质是一样的,都是在说明变量p是一个指针,在这串代码里对指针变量使用了解引用操作符(*),根据指针变量p里存放的地址找到了变量a , 然后在进行赋值就改变了变量a的值,将10改变为6。
三、指针变量类型的意义
3.1解引用操作符
例如:整型,整形在内存里占四个字,对一个整形取地址时取出的实际的它第一个字节的地址
将这个字节给指针变量保存时也是保存它的第一个字节,通过第一个字节来就可以找到其余内容,这是因为,整形的四个字节是连续存放的。
模拟数组下标引用操作符:
数组名代表着数组的地址,在讲数组的地址存放在指针变量里时如下:
int arr[] = { 1, 2, 3 };
int* p = arr;
(对数组更多详细理解在后续文章展现),
1、数组名表示着首元素的地址,对首元素进行解引用就可以找到它对应的值。
2、数组在内存里是连续存放的。
就可以依靠连续存放这个特点来模拟数组的下标引用操作符。
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* parr = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
可以发现输出结果是一致的,首先对于 *(arr + i) 这部分代码,我们使用arr + i 可以不断的向后寻找下一个数组元素所对应的地址,然后将其解引用就可以通过这个地址来找到它所对应的值。
效果等同于 arr[i]。
到了这,我们可以不使用整形指针来接受地址吗?可以这样做但需要知道这样左后会带来什么后果。
整形指针变量的类型表示,指针变量p指向地址的对象的类型是整形,我们进行访问时就按照4个字节1个整形,4个字节1整数进行访问,如果将上述代码使用char类型的指针变量,我们在访问地址时就是1个字节1个字节的访问。
总结:指针的类型给了指针在进行解引用时有多大的权限,能够访问多少个字节
char类型指针可以访问1个字节,int类型指针可以访问4个字节。
3.2指针+-整数
指针加减整数实质上是在操控当前的指针指向的地址是在哪
int main()
{
int a = 10;
int* p = &a;
char* pa = (char*)&a;
printf("&a = %p\n", &a);
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1);
printf("pa = %p\n", pa);
printf("pa + 1 = %p\n", pa + 1);
return 0;
}
有没有发现a的地址确实是存在指针变量p里它们的值是一样的,p+1由于它的类型是整形所以向后移动了4个字节地址的大小相加了4。由于&a的类型是整形指针我们把它赋给字符指针时需要强制类型转换,当我们把a的地址放在字符指针变量里是,打印的也是a的地址,pa + 1后由于它是字符,一次只能操作一个字节所以地址的大小加了1。
小结:指针的类型决定了指针向后(前)移动多少个字节。
四、const修饰的指针变量
4.1const修饰变量
直接上代码:
int main()
{
const int a = 10;
int const b = 9;//等价
a = 6;
b = 3;
return 0;
}
当使用const修饰变量后,变量会变为常变量,一个无法被再次赋值的变量,直接对const修饰的变量赋值,编译器会报错,因为它无法被修改。 但我们刚刚了解了指针,哎!可不可以使用指针变量来进行修改呢?使用指针变量,进行解引用时可以通过变量a的地址来对它进行修改,而不是直接重新赋值。不能光明正大的重新赋值,我把你的地址取出来,在重新赋值。
int main()
{
const int a = 10;
int const b = 9;
int* p = &a;
*p = 3;
printf("%d", a);
return 0;
}
4.2const修饰指针
基于上述代码,那将指针变量也使用const修饰它还可以改变变量a的值吗?
没错是这样,使用const修饰指针时分为三种情况,
void test1()
{
int a = 10;
int b = 6;
int const* p = &a;
&p = 3;
p = &b;
}
void test2()
{
int a = 10;
int b = 6;
int * const p = &a;
&p = 3;
p = &b;
}
void test3()
{
int a = 10;
int b = 6;
int const* const p = &a;
&p = 3;
p = &b;
}
int main()
{
test1();
test2();
test3();
return 0;
}
第一种情况,const放在*左边修饰,修饰指针指向的内容,无法对指针变量p解引用重新赋值,但可以取新的地址给p
第二种情况,const放在*右边修饰,修饰指针存放的地址,无法取新的地址取个指针变量p,但可以对p进行解引用。
第三种情况,const同时放在*的左右修饰,无法进行解引用,和取新的地址。
五、指针的运算
指针+指针是一个非法的操作,未被定义的,将两个指针的值相加,这在C语言中没有直接的语法。就好比如,月份+月份,这个结果是没有意义的,每个月的天数都不一定相同。
指针-指针的操作是合法的,并且返回一个整数,表示指针之间的元素差。
直接上实例,使用指针-指针模拟strlen函数:
size_t strlen ( const char * str );
cplusplus.com/reference/cstring/strlen/
更这个网址,得知strlen的返回值是size_t的无符号整形,在计算字符串长度是计算字符'\0'之前的字符个数,遇见’\0‘后停止,并返回当前的大小。
指针减指针,想一想我们可不可以使用字符串str最后一个字符的地址减去第一个字符的地址,返回的就是它们之间的元素个数。
size_t my_strlen(const char* str)
{
char* s = str;
while (*str)
{
str++;
}
return str - s;
}
int main()
{
char arr[] = "abcdefg";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
在my_strlen函数里我们先让str不断+1直到指向最后一个字符’\0‘,后就会停止循环,将最后一个字符的地址减去第一个字符的地址最终把这个值返回就得到一个字符串的元素个数。
while(*str),对str解引用得到的值就是当前str地址所对应的值,当str指向’\0‘的地址时,进行解引用得到的返回值为0,循环结束。
六、野指针
野指针,野指针没有明确指向空间的指针,不确定,随机性,没有预测的。
什么场景会出现野指针?
1、没有对指针变量初始化
int main()
{
int* a;
*a = 10;
return 0;
}
2、指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 12; i++)
{
*(p + i) = i;
}
return 0;
}
指针变量p越界访问了第十一个数组元素,实际的数组只存放了10个元素。
3、指针指向的空间被释放
char* get()
{
char arr[] = "hello world";
return arr;
}
int main()
{
char* str = NULL;
str = get();
printf(str);
return 0;
}
在接受get函数的返回值后字符数组指向的空间被释放,函数传递回来一个没有明确指向的地址,str就变为了一个野指针
如何避免野指针:
1、指针正前初始化
int main()
{
int a = 6;
int* p1 = 0;
int* p2 = &a;
int* p3 = NULL;
return 0;
}
2、使用指针访问空间要小心
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
return 0;
}
在使用指针时应小心,一个变量向程序申请了多少空间,使用指针时就应指向多大的空间。
3、对指针置NULL
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
p = NULL;
//代码………………
//……
//需要使用指针变量p
p = &arr[0];
if(p != NUNLL)
{
//
}
return 0;
}
七、assert断言
assert.h头文件指定了一个宏assert,用来判断程序运行时是否符合判断条件,如果不符合就报错结束程序,常称assert为断言。
用assert来判断野指针
int main()
{
char* str = NULL;
assert(str != NULL);
return 0;
}
assert宏在使用中可以接受一个表达式传值,如果返回的是真(非零)则程序正常运行,返回的是一个假(零),程序就会报错并终止运行,报错信息包含者文件名,表达式行号,错误信息。
在使用assert宏时,我们可以手动的启动或关闭assert宏的使用,在#include <assert.h>之前定义一个宏,#define NDEBUG,这就关闭了assert宏,在运行代码中含assert宏的表达式都会被编译器默认为空白的内容,在#define NDEBUG前加上注释就可以关闭assert宏。
assert宏在debug版本里可以使用,而在release版本里就不会有任何效果,assert宏被优化掉了,这样增加了程序的运行效率。
八、指针的应用传值调用和传地址调用
写一个函数实现对两整数的交换:
传值调用
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 5;
int b = 3;
swap(a, b);
printf("%d %d", a, b);
return 0;
}
如果你将上述代码运行就会发现整数a,b并没有发生交换,函数swap在接受实际参数时创建了两个形式参数,形式参数只是实际参数的一份临时拷贝,在函数了确实完成了两个整数的交换,但出函数后形式参数被销毁,实参没有发生任何改变,这是传值调用将,实际参数的值传递给形式参数。
传址调用 :
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 5;
int b = 3;
swap(&a, &b);
printf("%d %d", a, b);
return 0;
}
这里使用了整数a,b的地址作为实际参数传递给函数swap ,由于是地址所以形式参数使用指针变量来接收,在函数里都对指针变量x,y进行了解引用,对指针变量解引用可以通过地址来找到它所对应的值,这样对指针变量交换后,主函数里的变量a、变量b也发生了交换。打印结果与预期相符合。
九、二级指针
二级指针是用来存放指针变量的地址,好比如现在有三个抽屉,第一个抽屉存放的五个苹果,
第二个抽屉存放了第一个抽屉的钥匙,对第二个抽屉解引用就可以获得五个苹果,第三个抽屉存放了第二个抽屉的钥匙,需要对第三个抽屉解引用两次才能拿到五个苹果,如果只解引用一次拿到的是第二个抽屉的钥匙。
int main()
{
int a = 5;
int* pa = &a;
int** p = &pa;
**p = 3;
printf("%d", a);
}
这里没有在主函数里对字符变量p取地址,而是将字符变量p的地址传递给test函数由于传递的是一个指针变量的地址所以使用二级指针来接受,对二级指针变量p解引用一次就找到了指针变量p的地址然后将一个字符串,第一个字符的地址放在里面,现在char* p里存放的就个常量字符串。
void test(char** p)
{
*p = "hello";
}
int main1()
{
char* p = NULL;
test(&p);
printf("%s", p);
return 0;
}
完