C语言指针(1)

目录

一.内存和地址

1.内存

2.地址(指针)

3.计算机的编址

二.指针变量和地址

1.取地址操作符(&)

2.解引用操作符(*)和指针变量

 三.指针变量类型的意义

        1.指针的解引用

2.指针+-整数

四.void*指针

1.概念

2.作用

五.const修饰指针

1.const修饰变量

2.const修饰指针变量

六.指针运算

1.指针+-整数

2.指针- 指针 

3.指针的关系运算

七、野指针 

1.野指针的成因

(1)指针未初始化就使用

(2)指针越界访问

(3)指针指向的空间被释放

2.如何避免野指针

(1)在使用指针时要初始化指针,当不知道初始化为什么值的时候我们可以将他置为NULL。

(2)防止指针越界

(3)避免返回局部变量的地址

(4)当指针使用完之后要及时置为NULL,使用之前要检查它的有效性。

八、传值调用和传址调用

1.传值调用

2.指针的传址调用 

九、指针和数组

1.数组名

2.一维数组传参的本质

十、二级指针

1.概念

2.二级指针的运算

十一、指针数组

1.概念

2.声明与定义

3.指针数组模拟实现二维数组


一.内存和地址

1.内存

(1)内存:内存是用来存放机计算机CPU处理之前和处理之后的数据的。

(2)内存的管理

  1. 内存又被分为小单元格,每个小单元格的大小为一个字节。
  2. 为了方便对内存进行管理,我们对每个地址单元进行了编号,这样CPU就能通过编号快速找到一个内存空间。

2.地址(指针)

        (1)这些内存单元的编号就被称为地址。在C语言中,我们将这些地址叫做指针。

        (2)我们可以简单的理解为:内存单元编号 == 地址 == 指针

3.计算机的编址

        计算机的地址编号并不是真实存在的,而是由计算机的硬件实现的。计算机的CPU和内存之间是由地址总线连接的,而这也是硬件编址的基础。32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。地址被下达给内存,在内存上就可以找到该地址对应的数据,将数据通过地址总线传入CPU。

二.指针变量和地址

1.取地址操作符(&)

        取地址操作符是用来取出变量的地址的,因为每一个内存单元的大小是一个字节,&取出的是第一个字节的地址。

#inlcude <stdio.h>

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

    return 0;
}

 调试结果:

7683a4e6f0bc45f8ba488baff7bc31d6.png

 输出结果:

fd04fe0f4653452db992161952234216.png

        在上图中我们可以看见a的地址和它下面的地址相差4,因为a是整型,所以a占的字节是4个字节,程序打印的是第一个字节的地址。因为,当我们找到数据的第一个字节后,我们就可以根据他的数据类型找到他剩余的地址。

2.解引用操作符(*)和指针变量

(1)指针变量:指针变量是用来存放指针的地址的,通常与*连用。指针变量也是变量,存放在指针变量中的值都会被理解为地址。

#include <stdio.h>

int main()
{
    int a = 10;
    int *pa = &a;//取出a的地址放在pa中
    
    return 0;
}

(2)指针变量各部分的理解

        pa是指针变量的变量名;*说明pa是指针变量,int说明pa指向的数据是int类型(整型)。

(3)解引用操作符

        *解引用操作符可以通过指针变量中存放的地址找到所指向的空间,并对其中的值进行修改。

#include <stdio.h>

int main()
{
    int a = 25;
    int *pa = &a;
    *pa = 10;
    printf("%d\n",a);

    retrun 0;
}

输出结果: 

e61599a3c3ad467696ea48da18ad8b6b.png

         *pa的意思就是通过a的地址找到a变量的内存空间,当指针变量要通过它存放的变量的地址访问该变量时需要用*(解引用操作符)

(4)指针变量的大小

        指针变量在不同的环境下有所不同:

  1. 在32位环境下,指针变量的大小为4个字节
  2. 在64位环境下,指针变量的大小为8个字节
#include <stdio.h>

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

	return 0;
}

输出结果: 

e057856c0fbc45128060af86182b2072.png

c63af1d506104f5aafc18152cb4f642c.png

 结论:

  • 32位平台下的地址是32个bit位,指针变量的大小在32位环境下为4个字节
  • 64位平台下的地址是64个bit位,指针变量的大小在64位环境下是8个字节
  • 指针变量的大小和类型无关,在同一平台下指针变量的大小是相同的。

 三.指针变量类型的意义

        1.指针的解引用

          指针变量的类型决定了,对指针解引用时有多大的权限。

代码1:

//代码一
#include <stdio.h>

int main()
{
    int a = 0x11223344;
    int *pa = &a;
    *pa = 0;

    return 0;
}

调试结果如下: 

b4a800fc83dc4113ada22829051d27c7.png

92fd927f930e4c939a13b05db52be590.png

 代码2:

//代码二
#include <stdio.h>

int main()
{
	int a = 0x00789010;
	char* pa = (char*)&a;
	*pa = 0;

	return 0;
}

调试结果如下: 

b853ca6f79c840599239717e2563f354.png

23bb0e5fc60b4baea0ca845b29e24416.png

         通过对比代码一和代码二,我们不难发现代码1将a的四个字节全部改为了0,而代码2则只将a的一个字节改为了0。因此,对于不同类型的指针,它解引用后的权限也不一样。

        结论1:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作机个字节)。

2.指针+-整数

        指针+-整数可以跳过几个字节,但是指针跳过的字节个数不仅与+-的整数有关,还和指针的类型有关。

#include <stdio.h>

int main()
{
	int a = 20;

	int *pa = &a;
	char* pb = (char*)&a;

	printf("&a     = %p\n", &a);
	printf("pa     = %p\n", pa);
	printf("pa + 1 = %p\n", pa + 1);
	printf("pb     = %p\n", pb);
	printf("pb + 1 = %p\n", pb + 1);

	return 0;
}

输出结果: 

b297d69d1da640f4866446cf3d9d7c5d.png

         通过运行结果我们可以看到:char*类型指针+1只跳过了1个字节,int*类型指针+1跳过了4个字节。其实,指针+-1就是跳过一个指向指针的元素。

        结论2:指针+-整数决定了指针向前或向后走一步有多大。

四.void*指针

1.概念

        void*类型指针可以理解为无具体类型的指针(又叫作泛型指针),他可以接收任意类型的地址,但是他不能进行指针+-整数和解引用操作。

#include <stdio.h>

int main()
{
	int a = 30;
	char ch = 'w';

	void* pa = &a;
	void* pc = &ch;

	*pa = 10;
	*pc = 'e';

	return 0;
}

编译结果为:

30de34b0ebcf41ff83b875bd594125c9.png

        通过编译结果我们可以看见void*类型指针可以接收不同类型的地址,但是不能进行指针运算。 

2.作用

        void*类型指针主要用在函数参数部分,用来接收不同类型的指针,这样可以实现泛型编程的效果。具体实现在qsort函数中去讲。

五.const修饰指针

1.const修饰变量

        变量的值可以修改的,当const修饰变量之后,变量的值就不能够再修改了,这时该变量称为常变量,但是它的本质还是变量,如果强行改变则编译器会报错。

#include <stdio.h>

int main()
{
	int const a = 14;
	int n = 0;
	a = 20;
	n = 30;
	printf("%d %d\n", a, n);

	return 0;
}

输出结果为:

b58524d461a744619bbb707549ce519e.png

         编译器报错,a的值不能被修改。

但是我们可以绕过a来简间接修改a的值(用指针来修改):

#include <stdio.h>

int main()
{
	int const n = 15;
	printf("%d\n", n);
	int* p = &n;
	*p = 38;
	printf("%d\n", n);

	return 0;
}

输出结果为:

7c74edb285f2471cb4f01c1188d8ad63.png

2.const修饰指针变量

        const既可以放在*左边,又可以放在*右边,两者的效果有所不同。

        (1)const放在*右边

#include <stdio.h>

int main()
{
	int n = 15;
	int m = 21;
	printf("%d\n", n);
	int* const pn = &n;
	*pn = 32;
	printf("%d\n", n);
	pn = &m;

	return 0;
}

编译结果为:

ee19f59ccb9541d0a75101f7b025a6c5.png

        我们发现此时编译器报错,错误在第13行(pn = &m),因此我们可以知道:当const在*右边时,指针的内容不能被修改,即指针中存放的地址不能被修改,但是指针指向的变量的值可以被修改。 

      (2)const放在*左边

#include <stdio.h>

int main()
{
	int n = 15;
	int m = 21;
	const int* pn = &n;
	printf("%d\n", *pn);
	pn = &m;
	printf("%d\n", *pn);
	*pn = 32;

	return 0;
}

编译结果:

5d97ce2857e24751827bdefaf2f2de81.png

        此时,结果显示第13行(*pn = 32)有误。即,此时pn指向的变量的值不能够修改,但是pn中存放的地址可以修改。

#include <stdio.h>

int main()
{
	int n = 15;
	int m = 21;
	const int* pn = &n;
	printf("%d\n", *pn);
	pn = &m;
	printf("%d\n", *pn);

	return 0;
}

输出结果:

a24e084d7aca468099ab6d01ef7cd2c5.png

结论: 

  • 当const在*右边时,修饰的变量是指针本身,保证了指针变量的内容不能被修改,但是指针指向的内容,可以通过指针改变。
  • 当const在*左边时,修饰的变量是指针指向的内容,保证指针指向的内容不能通过指针来改变,到那时指针变量本身的内容可变。

六.指针运算

1.指针+-整数

        在数组中通过指针+-整数能够找到数组中其他的元素,从而达到遍历数组中每个元素的作用。

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pa = arr;
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}

	return 0;
}

输出结果为:

01662d927aa74d3596eddffdec027b97.png

2.指针- 指针 

        指针-指针得到的是两个指针之间相隔的元素个数,不是它们指向的元素之间的差值。

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa = &arr[0];
	int* pa1 = &arr[9];
	printf("%lld\n", pa1 - pa);

	return 0;
}

输出结果:

76c47742bc61426fb650583851137543.png

3.指针的关系运算

        指针的关系运算之主要是指针之间进行大小的比较

#include <stdio.h>

int main()
{
	int str[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(str) / sizeof(str[0]);
	int* p = str;
	int i = 0;
	while (p < str + sz)
	{
		printf("%d ", *p);
		p++;
	}

	return 0;
}

输出结果为:

32f07756c1594959b1b1f12ade75db54.png

七、野指针 

1.野指针的成因

(1)指针未初始化就使用

#include <stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	int* p;//指针未初始化默认为随机值
	*p = 10;
	printf("%d\n", *p);

	return 0;
}

编译结果为:

8635b953b82d46d6b3332fd9ced9a692.png

 我们可以看见:此时编译器报错。(使用了未初始化的局部变量p)

(2)指针越界访问

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i < sz + 3; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");

	return 0;
}

输出结果为:

a9dd3146a60146ab8d0ceb187757a392.png

        在该代码中,指针p对数组进行了越界访问,我们可以看见此时程序输出了几个小于0的数字。因此,我们要注意指针越界的问题,不然有可能造成程序崩溃。

(3)指针指向的空间被释放

#include <stdio.h>

int* test()
{
	int n = 10;
	return &n;
}

int main()
{
	int a = 25;
	int* p = test();
	printf("a = %d\n", a);
	printf("*p = %d\n", *p);

	return 0;
}

输出结果为:

7681cbb454644ec2a31df94ac745d821.png

         通过观察输出结果我们不难发现*p的值并不是返回的n的值10,这是因为当出了test函数之后,n的空间就被释放了,此时n的地址中放的是一个随机值。

2.如何避免野指针

(1)在使用指针时要初始化指针,当不知道初始化为什么值的时候我们可以将他置为NULL。

        当指针被置为NULL时,若强行使用则编译器会报错,代码如下:

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int* ps = NULL;
	int str[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(ps + i));//未对指针进行初始化就使用
	}

	return 0;
}

编译结果为:

dcb892f7d8674a05b0aac27c85b8d6e0.png

         这时,如果我们想使用ps指针,则要对其进行初始化。

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int* ps = NULL;
	int str[10] = { 1,2,3,4,5,6,7,8,9,10 };
	ps = str;//对指针ps进行初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(ps + i));
	}

	return 0;
}

输出结果为:

4a6926e371a04e568965ab6cb9bd4ea5.png

        这时我们可以看见编译器输出了正确的结果。因此,我们要对指针进行初始化或者直接置为NULL。

(2)防止指针越界

        用指针访问数组时要注意数组的范围,防止指针越界而造成错误。

(3)避免返回局部变量的地址

        每个局部变量都有自己的作用域,当离开局部变量的作用域时,局部变量的空间就会被释放,此时,局部变量的空间中存放的是随机值。

(4)当指针使用完之后要及时置为NULL,使用之前要检查它的有效性。

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,4,3,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pa = arr;
	//检查pa的有效性
	if (*pa != NULL)
	{
		int i = 0;
		for (i = 0; i < sz; i++)
		{
			printf("%d ", *(pa + i));
		}
	}
	pa = NULL;//将pa置为NULL

	return 0;
}

        我们也可以用assert宏来检验指针的有效性

        assert()接收一个表达式作为参数,如果表达式为真,则程序继续运行assert不会产生任何作用;若果表达式为假,assert()就会报错,在标准错误流stderr中写入错误信息,以及包含这个表达式的文件名和行号。assert包含在头文件<assert.h>中。

八、传值调用和传址调用

1.传值调用

        在调用函数时,将参数本身传递给函数的这种调用方式我们叫做函数的传值调用。

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 25;
	int c = Add(a, b);
	printf("%d\n", c);

	return 0;
}

输出结果:

5c40d7f5f82741388f1deff692f34148.png

结论: 

        实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参。
 

2.指针的传址调用 

        在调用函数时,将参数的地址传递给函数的这种调用方式我们叫做函数的传址调用。

#include <stdio.h>

void num_transform(int* pa, int* pb)
{
	int c = *pa;
	*pa = *pb;
	*pb = c;
}

int main()
{
	int a = 10;
	int b = 35;
	printf("交换前a = %d,b = %d\n", a, b);
	num_transform(&a, &b);
	printf("交换后a = %d,b = %d\n", a, b);

	return 0;
}

输出结果为:

055042f945ac401d9b1d6c164a5f9499.png

        在这里我们将a和b的地址传递给了函数num_transform ,然后通过a和b的地址改变了a和b中的值。

结论:

        传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数的变量。

九、指针和数组

1.数组名

        (1)数组名是首元素的地址

#include <stdio.h>

int main()
{
	int str[] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&str[0] = %p\n", &str[0]);
	printf("str     = %p\n", str);

	return 0;
}

输出结果为:

8b293e670aed4a45be2112cd2b38b06c.png

        我们可以看见str和str[0]的地址相同,数组名就是首元素的地址。但是,有两个例外:

(1)sizeof(数组名)

        sizeof中放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。

#include <stdio.h>

int main()
{
	int str[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(str);
	printf("%d\n", sz);

	return 0;
}

输出结果:

cc9d3e423b884c0c9b9d852a36fbf2c1.png

        由于每个整型的大小是4个字节,str数组中一共有10个整数,因此str的大小为40个字节。 

(2)&数组名

        这里的数组名也是表示的是整个数组的地址,取出的是整个数组的地址。

#include <stdio.h>

int main()
{
	int str[] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&str = %p\n", &str);

	return 0;
}

调试观察: 

829385d1951346abac5d1437c8fed4ed.png

输出结果为: 

407b0eaf3da540a3a9c758492fd86809.png

        通过调试我们发现:打印出的是首元素的地址 。在学习数组的时候我们知道数组在内存中是连续存放的。因此,我们只要知道了数组首元素的地址就能找到数组中所有元素的地址。

        &arr是整个数组的地址,因此&arr + 1会跳过sizeof(arr)个字节(一个数组的大小);而&arr[0] + 1只会跳过sizeof(arr[0])个字节(一个元素的大小)。

2.一维数组传参的本质

        一维数组传参传的是首元素的地址

#include <stdio.h>

void test(int arr[])
{
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("sz1 = %d\n", sz1);
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("sz  = %d\n", sz);
	test(arr);

	return 0;
}

输出结果为: 
 

6753a4935b15409097cd3165161a307f.png

        由于数组名是首元素的地址,数组传参的时候传的数组名,因此本质上数组传参传递的是数组首元素的地址。

结论:

        一维数组传参我们既可以用数组接收,也可以用指针来接收。

十、二级指针

1.概念

二级指针就是存放指针变量的地址的指针。

#include <stdio.h>

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

	return 0;
}

输出结果:

d034d6e125734bb7b23636bcdb7de8b5.png

        我们可以发现&pa和paa的地址是相同的,因为 pa为二级指针,它存放的是pa的地址。

2.二级指针的运算

(1) 找到一级指针的地址,对一级指针进行访问

#include <stdio.h>

int main()
{
	int a = 20;
	int* pa = &a;
	int** paa = &pa;
	printf("*paa  = %p\n", *paa);

	return 0;
}

(2)找到一级指针中存储的内容

#include <stdio.h>

int main()
{
	int a = 20;
	int* pa = &a;
	int** paa = &pa;
	**paa = 35;
	printf("a = %d\n", a);

	return 0;
}

输出结果为:

4ab2a0ea83ad40338babb1cc7601899b.png

十一、指针数组

1.概念

        存放指针的数组叫做指针数组 。

2.声明与定义

        

int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* str[3] = { arr1,arr2,arr2 };//指针数组的声明和定义

3.指针数组模拟实现二维数组

#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 sz = sizeof(arr1) / sizeof(arr1[0]);
	int* str[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < sz; j++)
		{
			printf("%d ", *(*(str + i) + j));
		}
		printf("\n");
	}

	return 0;
}

输出结果:

e4075b7c1e3d4876bc219cdfbf7f78c7.png

        *(str+i)访问的是str中的每个元素,str中的元素都是一维数组首元素的地址, 因此*(*(str+1)+j)访问的是每个一维数组中的元素。

数组指针模拟的二维数组并不是真正的二维数组,应为该”二维数组“的每一行并不是连续的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值