文章目录
指针的基本知识点(1)
观前提示:本人才疏学浅,理解能力有限,本文仅供参考,若有那里出错,欢迎指正!!
指针的基础概念
1.了解内存和指针
在了解指针前,我们因该搞清楚什么是内存。内存就好比一块块的区域,里面存放着需要用到数据,在计算机中的内存一般是以bit划分,每一bit就好比一块区域,这一块区域能存某些数据,而每一字节分配一个地址,也就是说分配内存的最小单位是字节(byte),最小传输单位是位(bit)。如图1.1
图.1中0x00000001是这一字节地址,一字节中有8个bit位。
在C语言中地址被叫做指针,所以***内存单元的编码 == 地址 == 指针***。
指针的使用
接下来我们就直接来使用指针试试吧
#include<stdio.h>
int main()
{
int a = 0;
int* p = &a;
printf("%p\n", p);
return 0;
}
在上述代码,我们把a取地址存放到p指针变量中,再以地址的形式打印出来,我们就会打印出a的地址(如果打印出的地址太长不方便观察,可以该为x86环境,地址会短,便于观察)。
我们继续来观察下内存中存放的情况如图.2
图.2中a开辟了4个字节,每一个字节有一个地址,&a得到的是地址中较小的那个。
指针变量和解引用操作符( * )
1.指针变量
在上述代码中我们提到把&a存放到p指针变量中,所以指针变量就是一种变量,这种变量是用来存放地址的,存放在指针变量中的值都会理解为地址。
2.解引用操作符( * )
我们已经在前面的代码中见过这一操作符( * ),如:int * ,这里的是表示p的类型是整形指针变量和我们接下来要讲的解引用操作符并不相同,下来看一段代码
int main()
{
int a = 10;
char b = 'W';
int* p1 = &a;
char* p2 = &b;
printf("%d \n", *p1);
printf("%c \n", *p2);
return 0;
}
该代码的输出结果是10和w,所以我们就能想到 ***** 解引用操作符的作用或许就是把指针变量中的值给取出。但是int和char类型不同,在内存中占的内存也不同,为什么 *** *就能整好取出一个int类型或char类型呢?我们把目光再回到int,p1的类型上我们就能理解原因:指针的类型决定了,对指针解引用的时候有多大权限(一次能够操作多少字节)。
*** **解引用操作符不仅可以取出也可以,同时也能够修改该地址处的数据例如接下来的代码
int main()
{
int a = 10;
int* p = &a;
*p = 20;
printf("%d ", *p);
return 0;
}
从代码中能看出通过解引用操作符,我们能够修改该地址的数据。
3.指针的大小
结论如下:
- 32为平台下地址是32个bit位,指针变量的大小是4个字节。
- 64位平台下地址是64个bit位,指针变量的大小是8个字节。
- 指针变量的大小与类型无关只于环境有关。
指针大小的原因本文不深入解释。
指针 + - 整数
指针变量的加减会根据指针变量的类型,跳过类型大小的字节数,例如以下代码
int main()
{
int arr[4] = { 1,2,3,4 };
int* p = &arr;
printf("%d\n", *p);
printf("%d\n", *(p+1));
printf("%d\n", *(p+2));
printf("%d\n", *(p+3));
return 0;
}
该代码输出结果为1,2,3,4 从结果来看p+1跳过了1个int类型,以此类推,面对char类型的+1,也就是跳过1个字节。
void* 指针
void* 指针,在往后的未知类型传参中会大量使用例如
print(void* p)
{
printf("%d ", *(int*)p);
}
int main()
{
int a = 10;
int* p = &a;
print(p);
return 0;
}
上述代码中我们给print函数传了指针变量p而在print函数中用void类型接收,这种情况适用于,比如这个print函数要接收不止一种类型的参数,而我们在使用的时候把这些参数都以void的类型接收这样就能以一个函数来处理多种类型。
const修饰指针
const有两种
-
const int p*
-
int const p*
两种的const的位置不同造成的效果也不同,我们先看代码
void test1()
{
int a = 10;
int b = 0;
const int* p = &a;
*p = 20;//编译器报错
p = &b;
}
void test2()
{
int a = 10;
int b = 0;
int* const p = &a;
*p = 20;
p = &b;//编译器报错
}
void test3()
{
int a = 10;
int b = 0;
const int* const p = &a;
*p = 20;//编译器报错
p = &b;//编译器报错
}
int main()
{
test1();
test2();
test3();
return 0;
}
上述代码中编译器会报错,提示表达式必须是可修改的值。这就表明
(1)const int * p 情况下p的值不能被修改。 (2)int * const p情况下p的值不能被修改。
test3中左右都有const所以p和p的值都不能修改。
野指针
概念:野指针就是指针指向的位置时不可知的(随机的、不正确的、没有明确限制的)
1.造成野指针的原因
1.指针未初始化
int main()
{
int* p;//指针没有初始化,默认随机值
*p = 20;
return 0;
}
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*(p++) = i;//超出数组指向数组外的指针,p就是野指针
}
return 0;
}
3.指向已经释放的空间
int* test1()
{
int a = 10;
return &a;
}
int main()
{
int* p = test1();//p指向&a而a已经释放,所以p野指针
return 0;
}
在使用指针的时候我们要避免野指针的出现,若果不用该指针要及时至空(p=NULL)
arrsrt断言
assert主要时用来判断指针是否为空
assert(p != NULL);
使用assert时要包含头文件<assert.h>
assert如果表达式为真不会产生任何效果,程序继续进行,当表达式为假的时候,assert会报错。
strlen函数的模拟实现
接下来我们用strlen函数模拟实现来熟悉下指针的使用
size_t my_strlen(char str[])
{
char* p = str;
assert(p);
int count = 0;
while (*p)
{
count++;
p++;
}
return count;
}
int main()
{
char str[] = { "hello world" };
size_t ret = my_strlen(str);
printf("%zd", ret);
return 0;
}
指针的基本知识点(2)
一维数组传参的本质
首先我们得了解数组名的本质,一般情况下数组名代表的是首元素的地址,也有两个例外
- sizeof(数组名):sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
还有一点我们得搞清楚,&数组名取出的地址和首元素地址在数值上并没有区别,只有在进行加减的时候才会有区别,例如:
int main()
{
int arr[10] = { 0 };
&arr + 1;//表示跳过整个数组
arr + 1;//表示跳过一个int类型
return 0;
}
接下来我们来了解下一维数组传参的本质,在之前的strlen函数模拟实现代码中我们能看到在把数组传递过去的时候我们用数组来接收,但是我们知到数组名代表首元素的地址,所以我们传参的时候,本质上就是传递了地址,因此,在形参部分我们也可以用指针来接收。
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[] = { 1,2,3,4 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
以上代码就是用指针来接收。所以一维数组传参的本质就是传递了地址。
二级指针
在之前的代码中我们所用到的指针都是一级指针,而一级指针中存放的是地址,指向某个数。而二级指针中存放的也是地址–>指针的地址,所以二级指针指向的是一个指针。
int main()
{
int a = 10;
int* p = &a; //int* 是p 的类型
int** pp = &p; //int** 是pp 的类型
return 0;
}
上述代码中pp就是二级指针。
相对的,还会有多级指针与二级指针同理。
指针数组
1.定义
指针数组顾名思义就是存放指针的数组
int main()
{
char* str[] = {"hello","world"};
printf("%p\n", str);
printf("%p\n", str+1);
return 0;
}
从上述代码中我们能够打印出来两个地址,一个是“hello”的首元素地址,一个是“world”的首元素地址,这两个地址寸放大str数组中,所以str就被称为指针数组。
2.利用指针数组来模拟二维数组
int main()
{
int arr1[3] = { 1,2,3 };
int arr2[3] = { 4,5,6 };
int arr3[3] = { 7,8,9 };
int* arr[3] = { arr1,arr2,arr3 };
int i, j;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
上述代码中能否成功打印1-9的数组呢?答案是能!我们首先定义了3给整形数组arr1,arr2,arr3,再把它们的首元素地址存放到指针数组arr中,这好像确实能够模拟二维数组,但是在两个循环里面为什么能够直接用arr[ i ] [ j ]来打印呢,明明不是二维数组。这就涉及到arr [ x ]计算机中会变成什么。
我们要知到[ ] 一个操作符,计算机在理解arr [ x ]的时候会把它看作*( arr + x ),所以arr [ x ] == (arr+x),那么arr [i] [j]== ( *(arr+i)+j )。这样就能够解释上述代码中的arr [ i ] [ j ]。
指针的基本知识点(3)
字符指针变量
我们首先看一段代码
int main()
{
char a = 'a';
char str[] = "hello";
char* p1 = &a;
char* p2 = str;
printf("%c ", *p1);
printf("%s ", p2);
return 0;
}
上述代码中p1是字符指针变量,p2也是字符指针变量。
我们再来看一道有趣的题目出自《剑指offer》一书中
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
该题的答案是:
str1和str2不相同很好理解:因为创建str1和str2一定是两个内存块,所以str1和str2的地址一定不会相同。而str3和str2为什们是同一块内存块呢?原因就是:*str3和 *str4,直接指向了"hello bit",而"hello bit"是常量字符串C\C++会把常量字符串存储到内存中的静态区,而静态区的内容是不能修改的,当有多个指针指向同一块字符串时,就没有必要再创建一块新的空间来存放相同的字符串,大家都用这一个就可以,因为也不能修改所以公用就行。
数组指针变量
1.数组指针的基本形式
我们前面讲了指针数组,是存放指针的数组,那么数组指针自然就是指向数组的指针。
基本形式如下
int(*p)[x] = &arr;
去掉数组名就能表示类型 int (*) [5],一个指针指向数组长度为5的整形数组。
2.数组指针和一般指针的比较
int main()
{
int arr[] = { 1,2,3,4,5 };
int(*p1)[5] = &arr;
int* p2 = arr;
printf("%p\n", p1);
printf("%p\n", p2);
p1++;
p2++;
printf("%p\n", p1);
printf("%p", p2);
return 0;
}
上述代码中第4行就是数组指针的基本类型,p1是数组指针,p2是整形指针,当两个指针各自增的时候就能看出p1跳过整个数组,而p2跳过一个整形。
3.二维数组传参的本质
我们知到一维数组的数组名代表首元素的地址,二维数组的数组名也代表首元素的地址,只不过首元素的地址是首行数组的地址例如
int main()
{
int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
int(*p1)[4] = arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(*p1 + i));
}
}
上述代码中用数组指针p1接收二维数组arr的数组名,*p1取出的是第一行数组,+ i 再解引用( * )就是遍历第一行数组的所有元素。同样我们可以用数组指针将二维数组所有值打印出来。
int main()
{
int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
int(*p1)[4] = arr;
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
{
printf("%4d ", *(*(p1 + i)+j));
}
printf("\n");
}
}
函数指针变量
1.函数指针的基本形式
函数指针:是个指针,指向函数,那么我们能够通过这一指针来调用这一函数。我们首先来看一看函数是否有地址。
void test()
{
printf("%d\n", 10);
}
int main()
{
test();
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
结果是:
10
003813D4
003813D4
由此我们能看出函数不仅有地址而且函数名和&函数名表示的都是函数的地址。且函数名和&函数名并没有区别。
函数指针的基本形式如下:
int (*p)(int x,int y)
表示函数指针p指向的函数有x,y两个参数,并且返回类型是int型。
2.函数指针的使用
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 6;
int (*p)(int, int) = add;
int ret = (*p)(a, b);// (p)(a, b) *号可以不写
printf("%d ", ret);
return 0;
}
上述代码中我们将函数地址放到函数指针p中,利用函数指针p调用add函数。
以上就是指针的基本知识点,当然指针中一定会有更加复杂的知识点,本文只讨论基础。若想看更多有关指针的知识,请尽情期待!