C语言学习之指针知识总结

2022-11-13 12:11:29 语言 知识 指针

一、地址

内存中的最小单元是字节,一个字节对应一个编号,这里的编号就是对应字节的地址。换句话说,地址就是内存单元的编号。

二、指针与指针变量

指针与指针变量是两个不同的概念,指针是某个普通变量的地址,所以可以理解为,指针就是地址,地址就是指针。指针变量是一种变量,它的作用是存储其它变量的地址。

#include <stdio.h>

int main()
{
	int * p; // int *是指针类型,p是对应的变量,定义的指针变量p只能用来存储int类型变量的地址
	int i = 3, j;
	
	p = &i; // 指针变量只能用来存储对应类型变量的地址,所以这里需要对变量i进行取地址操作,即&i
	
	
	printf("%d %d\n", *p, i);
	

	j = *p; // *p就是i,所以相当于将i的值赋给了j
	printf("i = %d, j = %d\n", i, j); // 输出结果为i = 3, j = 3

	return 0;
}

三、指针的作用

指针是C语言的灵魂

  • 通过指针可以表示一些复杂的数据结构,例如,链表、树、图等
  • 通过指针可以提高传输效率
  • 利用指针可以在被调函数中修改主调函数中的多个值
  • 利用指针可以直接访问硬件
  • 通过指针可以更方便地处理字符串
  • 指针是理解面向对象语言中引用的基础

四、初学指针时常见的错误

错误1:指针变量无明确指向

#include <stdio.h>

int main()
{
	int * p;
	int i = 5;
	
	*p = i; // 出错行
	printf("%d\n", *p);

	return 0;
}

错误原因:由于p未初始化,所以p的内容是垃圾值。*p是以p的内容为地址的变量,即以一个垃圾值为地址的变量,该变量是未知的,所以当把i的值赋值给p指向的未知变量时,有可能会篡改内存中其它变量的值,而这样的操作是不允许的。

错误2:赋值时变量类型不一致

#include <stdio.h>

int main()
{
	int * p;
	int * q;
	int i = 5;
	
	p = &i;
	
	*q = p; // 出错行
	printf("%d\n", *q);

	return 0;
}

错误原因:由于p是指针类型变量,而*q是int类型变量,所以不能相互复制。

五、通过调用函数修改主调函数中的值

思考1:在下述程序中,f函数中的变量i与main函数中的变量i是不是同一个变量?

f函数中的变量i与main函数中的变量i都属于局部变量,只在自己对应的函数中起作用,所以,f函数中的变量i与main函数中的变量i不是同一个变量。

#include <stdio.h>

void f(int i)
{
	i = 99;
}

int main()
{
	int i = 66
	printf("%d\n", i);
	
	f(i);
	printf("%d\n", i);

	return 0;
}

思考2:在上述程序中,能否通过调用f函数修改main函数中变量i的值?

由于f函数中的变量i与main函数中的变量i不是同一个变量,所以把实参i传递给形参i只会改变f函数中变量i的值,当f函数执行完毕后,分配给形参i的空间会被释放,故而无法改变main函数中变量i的值。换句话说,f函数中的变量i与main函数中的变量i本质上没有任何关系,所以不管怎么修改f函数中变量i的值都不会影响main函数中变量i的值。

那要如何才能通过其它函数来修改主调函数中的值?

此时,指针就派上用场了,如下述程序:

#include <stdio.h>

void f(int * p) // 通过接收地址来确定要修改的变量
{
	*p = 99; // *p就是以p变量的内容为地址的变量,也就是要通过该函数修改的变量
}

int main()
{
	int i = 66
	printf("%d\n", i); // 输出结果为66
	
	f(&i); // 由于函数f的形参是指针变量,故需将变量i的地址发送过去
	printf("%d\n", i); // 输出结果为99

	return 0;
}

上述程序可以实现在被调函数中修改主调函数中变量的值是因为通过向被调函数传递了需要修改的变量的地址,从而确定并指向了需要修改的变量,但如果不传入地址,就会导致主调函数中的变量无法与被调函数产生关联,从而无法实现目的。

活学活用:自定义一个swap函数,用该函数互换main函数中的两个变量的值

常见错误:只传入数值,不传入地址

void swap(int a, int b)
{
	int t;
	
	t = a;
	a = b;
	b = t;
}

int main()
{	
	int a = 3, b = 5;
	
	swap(a, b);
	
	printf("a = %d, b = %d\n", a, b);

	return 0;
}

出现上述错误的原因是,main函数中的变量a和b与swap函数中的形参a和b无关,导致的结果是,主函数将3和5发送给形参a和b后,swap函数只是对3和5进行了操作,而未能对main函数中的变量a和b进行操作,所以无法互换main函数中的变量a和b的值。

正确实现方法:传入地址,定位需要互换的变量

void swap(int * p, int * q)
{
	int t;
	
	t = *p;
	*p = *q;
	*q = t;
}

int main()
{	
	int a = 3, b = 5;
	
	swap(&a, &b);
	
	printf("a = %d, b = %d\n", a, b);

	return 0;
}

思考:如下方法是否可以实现互换功能?

#include <stdio.h>

void swap(int * p, int * q)
{
	int * t;

	t = p;
	p = q;
	q = t;
}

int main()
{	
	int a = 3, b = 5;
	
	swap(&a, &b);
	
	printf("a = %d, b = %d\n", a, b);

	return 0;
}

答案是不行的,上述程序将变量a和b的地址发送给了指针变量p和q,此时,变量p和q中储存的是变量a和b的地址,然而,swap函数中的操作是互换变量p和q的内容,也就是说,当swap函数执行完毕后,变量p中储存的是变量b的地址变量q中储存的是变量a的地址,言下之意,只是将变量p和q的内容互换了而已,并没有对main函数中的变量a和b进行操作,所以无法实现互换功能,此外,几乎所有的编程语言都无法通过互换两个变量的地址实现互换变量中的值。

六、指针与一维数组

一维数组的数组名

一维数组的数组名是一个指针常量,该常量是一维数组中第一个元素的地址。

#include <stdio.h>

int main()
{
	int a[5];
	int b[5];
	
	a = b; // 错误,因为a和b都是常量,所以无法进行赋值操作
	a = &a[2]; // 错误,因为a是常量,无法对一个常量进行赋值操作
	
	return 0;
}
#include <stdio.h>

int main()
{
	int a[5];

	printf("%#X", &a[0]);
	printf("%#X", a); // 与上一行输出结果相同,因为一维数组名就是数一维组中第一个元素的地址

	return 0;
}

引用一维数组中的元素

通过下标引用:如a[i]表示第i+1个元素

通过指针引用:如*(a+i)表示第i+1个元素

#include <stdio.h>

int main()
{
	int a[5];
	int i;
	
	for (i = 0; i < 5; i++) // 向一维数组中读入元素
		scanf("%d", &a[i]); // 通过下标引用数组中的元素

	for (i = 0; i < 5; i++) // 输出一维数组的内容
		printf("%d ", *(a + i)); // 通过指针引用数组中的元素

	return 0;
}

指针变量的运算

指针变量不能相加,相乘以及相除,只能进行相减。如果两个指针变量指向同一块连续空间的不同存储单元,则这两个指针变量才可以进行相减运算。两个指针变量相减得到的结果是两个指针变量间相隔的元素个数。

#include <stdio.h>

int main()
{
	int a[5];
	int * p = &a[1];
	int * q = &a[4];

	printf("%d\n", q - p); // 输出结果为3,证明相隔3个元素

	return 0;
}

七、使用函数操作一维数组

使用函数对一维数组进行操作,首先要将数组名传递给函数,因为一维数组名是函数第一个元素的地址,传递数组名就相当于传递起始位置,其次,普通数组不同于字符数组,它们没有结束的标志,所以还需要向函数传递数组长度以确定数组何时结束。故想要在另外一个函数中对一维数组进行操作需要向该函数传入两个参数,数组名和数组长度。

定义函数时的形参有两种写法,第一种是(int a[], int length),第二种是(int * a, int length)。可以写第二种的原因是一位数组名本身就是指针常量,所以可以直接用指针变量来接收。

定义一个函数,该函数的功能是对一维数组的内容进行输出

#include <stdio.h>

// 自定义的print函数,其功能是将一维数组输出
void print(int a[], int length)
{
	int i;

	for (i = 0; i < length; i++)
		printf("%d ", a[i]); // 也可以写成printf("%d ", *(a+i));
	
	printf("\n");
}

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int b[6] = { -1, -2, -3, -4, -5, -6 };
	int c[100] = { 23, 88, 99, 44 };

	print(a, 5);
	print(b, 6);
	print(c, 100);

	return 0;
}

八、指针变量所占字节数

预备知识:sizeof运算符的用法

sizeof(数据类型):其值为对应数据类型所占的字节数

例如:sizeof(int)值为4;sizeof(double)值为8;sizeof(char)值为1

sizeof(变量):其值为对应变量所占的字节数

#include <stdio.h>

int main()
{
	char c = 'A';
	int i = 99;
	double x = 66.66;

	char * p = &ch;
	int * q = &i;
	double r = &x;

	printf("%d %d %d\n", sizeof(c), sizeof(i), sizeof(x)); // 输出结果为1 4 8
	printf("%d %d %d\n", sizeof(p), sizeof(q), sizeof(r)); // 输出结果均为4

	return 0;
}

上述程序证明,尽管普通类型变量所占的空间大小不一致,但它们对应的指针变量都占四个字节。

九、静态数组的缺陷

数组长度必须事先指定,且只能是常整数,不能是变量

数组的长度在长度不能在函数执行过程中动态的增减,数组一旦定义,其长度就无法改变

程序员无法手动释放静态数组的内存,数组一旦定义,操作系统为该数组分配的存储空间就会一直存在,直到该数组所在的函数执行完毕后,该数组的空间才会被操作系统释放

在某个函数内定义的静态数组在该函数执行期间可以被其它函数使用,但当该函数执行完毕后,该函数中定义的数组就无法在其它函数中使用,这是因为该函数执行完毕后,静态数组的内存就被会被释放

十、malloc函数

malloc这个词是由memory(内存)与allocate(分配)这两个单词合成的,顾名思义,malloc函数就是用来分配内存的函数。

#include <stdio.h>
#include <malloc.h>

int main()
{
	int i; // 静态分配了4个字节的存储空间

	int* p = (int*)malloc(4);
	

	free(p); 
	

	return 0;
}

十一、动态数组的构造

#include <stdio.h>
#include <malloc.h>

int main()
{
	int a[10]; // 静态构造一维数组
	int length;
	int* pArray;
	int i;
	
	scanf("%d", &length);

	pArray = (int*)malloc(sizeof(int) * length);
	

	// 对该动态一维数组手动赋值
	for (i = 0; i < length; i++)
		scanf("%d", &pArray[i]);

	// 输出该动态一维数组的内容
	for (i = 0; i < length; i++)
		printf("%d ", *(pArray + i));
	
	printf("\n");

	free(pArray); // 释放该动态数组

	return 0;
}

十二、静态内存与动态内存的对比

静态内存是由操作系统自动分配,自动释放的。静态内存是在栈中分配的;动态内存是由程序员手动分配,手动释放,但如果只分配不释放就会导致内存泄露,也就是内存越用越少。动态内存是在堆中分配的。

十三、多级指针

#include <stdio.h>

int main()
{
	int i = 10;
	int * p = &i;
	int ** q = &p;
	int *** r = &q;
	
	r = &p; // 错误,因为r是int *** 类型,它只能用来存储int ** 类型变量的地址
	
	printf("i = %d\n", ***r);

	return 0;
}

表解上述程序:

变量名变量地址变量内容
i1000H10
p2000H1000H
q3000H2000H
r4000H3000h
变量名对应变量
*rq
**rp
***ri
#include <stdio.h>

void g(int ** q) // 由于p的类型是int *,所以q的类型必须是int **,因为q要用来存放p的地址
{
	
}

void f()
{
	int i;
	int * p;
	
	p = &i;
	
	g(&p); // 要通过g函数修改p的内容,则必须发送p变量的地址
}

int main()
{
	f();

	return 0;
}

十四、跨函数使用内存

由于静态内存是在栈中分配的,而函数执行完毕后,栈中的静态内存就会全部出栈,而动态内存是在堆中分配的,当函数执行完毕后,堆中分配的内存并不会像栈中分配的内存一样直接被释放掉,所以

静态内存是不能跨函数使用的,而动态内存是可以的。

思考:下述程序是否有语法错误?是否有逻辑错误?

#include <stdio.h>

void f(int** q)
{
	int i = 5;

	*q = &i; // 由于q储存了p的地址,所以*q就是p,这行代码实质是将i的地址赋给了p
}

int main()
{
	int* p;

	f(&p);

	printf("i = %d\n", *p); // 由于p储存了i的地址,所以*p就是i

	return 0;
}

上述程序没有语法错误,但是有逻辑上的错误,这是因为,当f函数执行完毕后,f函数中所有的静态变量的内存都会被释放掉,所以当执行到printf("i = %d\n", *p);时,p所指向的变量空间的访问权限已经返还给了操作系统,这样就会导致*p访问了不属于该程序的空间。这个程序说明了在一个函数内部定义的静态变量在该函数中执行完毕后就不再可以垮函数使用。

思考:对比上一程序,下述程序是否有语法错误?是否有逻辑错误?

#include <stdio.h>

void f(int** q)
{
	*q = (int*)malloc(sizeof(int));
	
	**q = 5;
}

int main()
{
	int* p;

	f(&p);

	printf("%d\n", *p);

	return 0;
}

上述程序是完全没有语法错误的,因为当f函数执行完毕后,其中分配的动态内存不会自动释放,所以在main函数中依然可以使用这段内存。这个程序体现出,在一个函数中分配的动态存储空间在该函数执行完之后,仍然可以在另外一个函数中使用。

趁热打铁:下列四个程序中,哪个程序能够通过调用fun函数使main函数中的指针变量p指向一个合法的整型单元?

#include <stdio.h>

void fun(int * p)
{
	int s;
	p = &s;
}

int main()
{
	int * p;

	fun(p);

	return 0;
}
#include <stdio.h>

void fun(int ** p)
{
	int s;
	*p = &s;
}

int main()
{
	int * p;

	fun(&p);

	return 0;
}
#include <stdio.h>
#include <malloc.h>

void fun(int * p)
{
	p = (int *)malloc(sizeof(int));
}

int main()
{
	int * p;

	fun(p);

	return 0;
}
#include <stdio.h>
#include <malloc.h>

void fun(int ** p)
{
	*p = (int *)malloc(4);
}

int main()
{
	int * p;

	fun(&p);

	return 0;
}

以上就是C语言学习之指针知识总结的详细内容,更多关于C语言指针的资料请关注其它相关文章!

相关文章