C语言指针pointer基础知识

        简单来说,一个指针是计算机内存这家酒店中某一间房间的门牌号,使用"*"解引用符号这把钥匙就能打开那个房间,找到储存在那个房间里面的数据。

一、指针变量和地址

1、创建变量的实质

       创建指针变量相当于我向计算机内存这个酒店申请一个房间,我可以叫它“我的房间”,也可以叫它“VIP休息室”,随便我怎么叫,对于内存来说都无所谓,因为这只是为了我能记住它,这些称谓其实就是变量名。

int main()
{
    int VipRestRoom = 10;
    // 意思是向内存申请4个自己的空间,用来存储10;空间名称叫VIPrestRoom,只是给程序员看的,随意取的名称。
    return 0;
}

计算机中存储的是二进制,10的二进制是0000 0000 0000 0000 0000 0000 0000 1010

                            对应的十六进制为:0x 0       0       0        0       0       0      0        a

也就是0x0000000a,通过在VS环境下调试查看内存时是会倒过来存储的。


VS下查看内存:①先进入调试 -> “窗口” -> “内存”,随便选一个内存选项

(这里补充说一下,有些同学VS的快捷键与电脑的快捷键冲突时,可能按F5不能直接进入调试,那么就需要手动去工具栏中的“窗口”下点击“逐语句”/“逐过程”按钮了。)

②输入取地址符号&后面跟上变量名,此处“列”下拉选择只显示4列

③输入之后回车就可显示内存数据(以16进制类型显示) 


 2、理解指针变量

       上文已经说到过了取地址操作符&,可以理解为像"$"这样的东西要给到酒店前台,才能拿到我的门牌号。但是当我拿到“门牌号”0x0000004618F3FBF4时,大吃一惊,这谁记得住啊,于是我要创建一个指针变量 p 存起来,p 多简单,一下子记住了。

&VipRestRoom;
int * p = &VipRestRoom;

这里的 p 就是存放指针的变量,类型是 int* 。 

int*中的 * 是在说明 p 是一个指针变量,int 是说明 VipRestRoom 类型是 int 。

那如果是 *&a 呢?这是啥意思?其实相当于抵消掉了,取地址和解引用抵消掉了(*&a == a的意思)。

3、解引用操作符*

       就像刚开始时我说的用"*"这把钥匙打开门牌号,就是解引用操作符 * 的作用。

int main()
{
    int a = 10; // VipRestRoom名字太长了,还是改为a更方便
    int* p = &a;
    *p = 0; // 通过*找到p所指向的a变量,并将a的值改为0
    printf("%d\n", a);
    return 0;
}

如果拿到这把钥匙,可以对房间里面的数据进行操作,或者不操作,只是打印出来【pritnf("%d\n", *p);】。

       上述代码是将数据进行了操作,修改了原始 a 变量的值。就像我的房间a里本来有10块钱,某个小偷拿到了钥匙*,也找到了我的门牌号 p,将我的钱偷走了,我的房间就只剩下0块钱了。为了防止指这种强盗行为,C语言中可以用 const 修饰指针变量。

4、const 修饰指针

       const 是常属性的意思,变量如果用 const 来修饰就不能被修改。

但是可以绕着弯修改变量 a 的值:

int main()
{
	const int a = 10;
	int* pa = &a;
	*pa = 20;
	printf("%d\n", a);
	return 0;
}

如果不想通过取 a 变量的地址,再进行解引用来间接修改变量 a 的值,那么指针变量也要加上 const 进行修饰。

       对于指针变量一旦加上const,其指向的变量就不能被修改,但指针指向的那个本质上还是变量,只不过不能通过解引用指针变量从而修改它的值了。(而在C++中,如果用 const 修饰一个变量,那该变量就不再是变量,直接就是常量。)

还要注意 const 修饰指针时放置的位置,其位置不同,修饰的对象也有所不同。

const 可以放在 * 的左边:const int* pa = &a; 或者 int const * pa = &a;

const 也可以放在 * 的右边:int* const pa = &a;

当 const 放在 * 左边时
int main()
{
  const int a = 10;
  int b = 20;
  const int* pa = &a;
  *pa = 20; // error
  pa = &b; // ok
  printf("%d\n", a);
  
  return 0;
}

        此时 const 限制的是 pa 所指向的对象,也就是 *pa 不能修改;但 pa 不受限制,也就是指针变量可以改变存储地址,从而改变指向。

 当 const 放在 * 右边时
int main()
{
  const int a = 10;
  int b = 100;
  int* const pa = &a;
  *pa = 20; // ok
  pa = &b; // error
  printf("%d\n", a);
  
  return 0;
}

        此时 const 限制的是 pa,也就是 pa 的指向不能改变;但是 *pa 不受限制,也就是说 pa 指向的内容是可以通过 pa 来改变的。

 二、指针变量的大小

       前面只提到 int* 类型的指针,但数据类型除了 int 类型,还要 char short long等类型。那它们的数据类型大小有1个字节、4个字节或者8个字节,指针变量类型也会是对应数据类型的内存大小吗?我们用 sizeof 操作符来打印查看一下。

int main()
{
	printf("%zu\n", sizeof(int*));
	printf("%zu\n", sizeof(char*));
	printf("%zu\n", sizeof(short*));
	printf("%zu\n", sizeof(long*));
	printf("%zu\n", sizeof(long long*));
	printf("%zu\n", sizeof(float*));
	printf("%zu\n", sizeof(double*));

	return 0;
}

 可见,在x64环境下,也就是64位平台下是64个bit位时,指针变量大小都是8个字节

           在x86环境下,也就是32位平台下是32个bit位时,指针变量大小都是4个字节

因此,指针变量大小和变量类型无关,那指针变量类型有什么意义?

三、指针变量类型的意义

 1、指针的解引用(较少涉及)

我们创建一个16进制的整型变量,目的是申请一个占满4字节内存的空间(4*8=32,占满了二进制位)

int main()
{
    int a = 0x11223344;
    return 0;
}

分别通过创建不同变量类型的指针变量,并对指针变量解引用之后进行修改 a 的值:

 

 对 int* 类型的指针变量解引用之后重新赋值之后改变了4个字节

 

用 char* 类型的指针存放 a 的地址,当然可以存放,只要是地址,内存大小都是4/8字节。但是对 char* 类型的指针变量解引用之后重新赋值之后却只改变了 a 变量的1个字节。

上述用 int* 和 char* 类型的指针变量访问a变量时都没有问题,但用 double* 类型的指针去访问就会越界,因为 double* 一次要访问8字节,而 a 只有4个字节,因此越界访问就会报错。

得到的结论就是——指针的类型决定了对指针解引用的时候有多大权限/一次能对变量操作几个字符。

2、指针的+-整数

对指针变量进行+-操作,是要跳过多少个字符的问题。

int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	printf("pa = %p\n", pa);
	printf("pc = %p\n", pc);

	return 0;
}

%p的打印地址的占位符。程序运行之后输出的两个地址一致,但出现警告词:“初始化”: 从“int *”到“char *”的类型不兼容。

 如果对指针变量进行+1的操作:

对 int* 类型的指针变量+1,跳过了4个字节;对 char* 类型的指针变量+1则只跳过1个字节。也就是访问的下一个字节与之前指向的字节的距离就是指针类型对应的数据类型大小。

得到的结论——指针的类型决定了指针向前或者向后走一步的距离跨越有多大

(这里需要提醒一个特殊类型指针是不能进行+-操作的,这个类型是 void*,此类的指针可以接收任意类型的地址,一般用在函数参数的部分。)

指针变量+-整数的应用

1、使用指针打印一维整型数组的所有元素 
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = &arr[0]; // 取出数组首元素的地址并存放到p这个指针变量中
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

  · 这里需要注意 int* p = &arr[0];  和  int* p = &arr; 的区别,arr是数组名对吧,通常在数组中都表示为数组首元素的地址,但是如果它单独与&放在一起,这里就不是首元素地址了,而是整个元素的地址!如果此时将整个数组的地址进行+-整数的操作,跳过的是40个字节,也就是将整个数组都跳过。所以又有 arr == &arr[0]; 它两等价。

上面程序中每一次执行p++,都修改了p的值,如果不修改p可以写成下面的for循环:

for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}

 也就是改变的只有 i 的值。


以上是将数组元素正序打印出来,那么如果想要将数组元素以逆序的形式打印出来有两种方法:

①改变指针变量

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = &arr[sz - 1]; // 指针变量存放的是数组最后一个元素的地址

	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p - i)); // 注意此处是 - 
	}
	return 0;
}

②改变 i 值

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = &arr[0];
	for (i = sz-1; i >= 0; i--) // 注意这里i不再是++了
	{
		printf("%d ", *(p + i));
	}
	return 0;
}
2、使用指针打印字符串数组

还记得双引号创建的字符串最后隐藏着一个 \0 吗,一定要记得啊!!!

所以如果用循环实现打印字符串数组所有的字符,使用for循环是不是没有使用while循环方便呢?因为打印字符串的时候遇到 \0 就停止了,只有一个条件满足就不再循环了。

int main()
{
	char arr[] = "hello world";
	char* p = &arr[0];
	//while (*p != '\0')
    // 因为\0的ASCII码值为0,在while循环的表达式中也可以直接写成(*p)
    while (*p)
	{
		printf("%c ", *p);
		p++;
	}
	return 0;
}

 四、指针 - 指针

       两个指针也可以直接进行运算,但前提是两个指针指向同一块空间,否则不能相减。最常用的就是使用在数组中,指针 - 指针的绝对值得到的是两指针之间的元素个数。

为什么说绝对值,因为在内存中是从低地址向高地址进行存储的,所以 低地址 - 高地址 = 负数。

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
    printf("%d\n", &arr[10] - &arr[0]);
	return 0;
}

 打印结果:

 因此我们可以使用指针 - 指针的方式计算出数组的长度,但我们已经有一个可以计算字符串长度的库函数 strlen() 了,我们可不可以自己实现一下计算数组长度的功能呢?

 实现思路:

计算没遇到 \0 之前对存放了首元素地址的指针变量可以加多少次1

size_t my_strlen(char arr[])
{
	size_t count = 0; // 用size_t无符号整型修饰的原因:次数肯定>=0
	char* p = &arr[0];
	while (*p) // 如果元素不为0,则进入循环
	{
		count++;
		p++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	printf("%zu\n", my_strlen(arr));
	return 0;
}

此处需要注意数组名 arr 的本质意思。arr 除了在&之后以及单独放在sizeof内时表示整个数组外,其他情况下都是指首元素的地址的意思!即 arr == &arr[0]; 所以给函数传参时,传送的是一个地址类型,因此在创建函数时给形参的命名除了是数组的形式,也可以是指针变量的形式。

因此对上面的函数进行优化:

size_t my_strlen(char* p)
{
	size_t count = 0;
	while (*p)
	{
		count++;
		p++;
	}
	return count;
}

 啊?等等,你没有用到指针 - 指针的知识啊?别着急,上面的只是一个引子。我们该怎么用指针 - 指针的方式计算数组长度呢?其实只要我们用 \0 之前的指针减去首元素的指针就能够实现。

size_t my_strlen(char* p)
{
	char* start = p;
	while (*p) //使用p来找到字符串中的\0
	{
		p++;
	}
	return p-start;
}
int main()
{
	char arr[] = "abcdef";
	printf("%zu\n", my_strlen(arr));
	return 0;
}

五、指针关系运算

指针和变量一样,也是可以进行比较的,本质就是地址之间的比较。例如:

等价比较

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[0];
	if (p == arr)
		printf("true");
	return 0;
}

由上述程序可见,arr 和 &arr[0]是等价的,也就是 arr 就是首元素的地址,除了那两个特殊情况,还记得是什么吗?

①单独放在sizeof内:sizeof(arr);

②取地址符号 &arr。

上面两种情况是表示整个数组哦!


 利用大小比较输出数组元素

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[0];
	while (p < arr + sz)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

总结tips

        以上就是C语言指针的基础知识,重点其实是数组名表示的意思,通常是首元素的地址(即 arr == &arr[0];),但有两个特例是表示整个数组。

&arr 取得的是整个数组的地址,对其+1得到的就是跳过整个数组之后的地址;

还需注意的是

       sizeof(arr); 里面的 arr 表示整个数组,因此 sizeof 计算得到的是整个数组的内存大小,
比如数组类型是整型,存放了5个数据,那么 sizeof(arr) 的结果是20。

       但如果是 sizeof(arr+0); arr没有单独放在sizeof内此时就是首元素的地址,而且+0本质上没有动,还是首元素的地址,计算地址的内存大小就是4或者8个字节。

       如果是 sizeof(arr+1);   arr+1是首元素地址跳过一个数组元素之后的地址,即数组中第二个元素的地址。但由于地址的内存大小都是4或者8个字节,所以其计算结果也还是4或者8。

       记住!所有地址用sizeof进行计算的时候得到的都是4或者8,单位是字节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值