[C/C++]指针详讲-让你不在害怕指针

  •  个人主页:北·海
  •  CSDN新晋作者
  •  欢迎 点赞✍评论⭐收藏
  • ✨收录专栏:C/C++
  • 希望作者的文章能对你有所帮助,有不足的地方请在评论区留言指正,大家一起学习交流!

目录

前言

 一.&与*的作用与结合方向

二.指针和数组的关系

1.利用指针访问数组里的值

2.利用指针将数组作为实参传递给函数

 三.字符指针与字符数组

四.指针在内存分配中的应用

1.用C语言中的malloc在堆区分配动态内存

1.用C++中的new在堆区分配动态内存

五.使用指针为什么能提高程序的性能

六.指针与const搭配的四种情况

七.引用是实现原理

八.多级指针

九. 指针和函数的关系

1.通过指针使函数返回多个值

2.函数指针

3.函数的返回值类型为指针类型

十.指针和结构体的关系

 


前言

指针在编程语言中扮演着重要的角色,特别是在C语言中。指针提供了直接访问和操作内存地址的能力,使得编程更加灵活、高效,并且可以处理各种复杂的数据结构和算法。

以下是指针的几个重要方面和作用:

  1. 内存管理:指针允许程序直接与内存交互。通过使用指针,可以动态地分配和释放内存,避免了静态内存分配所带来的限制。这在处理动态数据结构(如链表、树、图等)和大规模数据中非常有用。

  2. 传递参数和引用:通过指针,可以在函数之间传递参数和引用。传递指针作为函数参数,可以避免在函数调用时产生副本,节省了内存和时间开销。此外,指针还可以用于函数返回多个值或修改调用者的变量。

  3. 动态数据结构:动态数据结构如链表、树、堆等通常需要使用指针进行内存分配和组织。指针可以在运行时创建、删除、连接和重新组织数据结构,使得数据结构的管理更加灵活和高效。

  4. 数组和字符串操作:在C语言中,数组实际上是通过指针来访问和操作的。指针使得数组可以直接访问和修改其元素,还可以进行指针算术操作,实现数组的遍历和操作。字符串在C语言中本质上是以空字符结尾的字符数组,指针的使用是对字符串进行处理的关键。

  5. 性能优化:指针的使用可以提高程序的性能。通过使用指针来访问和操作数据,可以减少副本的生成和数据的复制,提高程序的执行效率。


 一.&与*的作用与结合方向

作用: &在等号的右边为取地址符号,在等号的左边为引用符号

         *可以用做为乘号,也可用作为对指针变量解引的符号,取该地址存放的数据

结合方向:

“&”和“*”都是右结合的。假设有变量 x = 1,则*&x 的含义是,先获取变量 x 的地址,再获取地址中的内容。因为“&”和“*”互为逆运算,所以 x = *&x。

例如:输入 x、y 两个整数,然后将其中的值大的赋值给 x,小的赋值给 y。即:假设输入 x = 8,y = 9。就将 9 赋值给 x,8 赋值给 y。

void main(){
	//声明两个普通变量
	int x, y;
	//声明两个指针变量
	int *px, *py;
	//声明一个临时变量,用于交换
	int t;
	//输入两个值,赋值给 x、y
	scanf("%d", &x);
	scanf("%d", &y);
	//给指针变量 px、py 赋初值(关联变量 x、y)
	px = &x;
	py = &y;
	//利用指针来对比 x、y 的值,如果 x 的值比 y 的值小,就交换
	if(*px < *py){
		//交换步骤,其中*px == x、*py == y
		t = *px;
		*px = *py;
		*py = t;
	}
	printf("x =  %d, y = %d", *px, *py);
}

二.指针和数组的关系

1.利用指针访问数组里的值

	//利用指针访问数组里面的值
	int nums[] = {1,2,3,4};
	//方法一:下标法
	cout << "第一个数 :" << nums[0] << endl;
	//方法二:指针法
	cout << "第一个数 :" << *(nums + 0) << endl;//数组名就是数组元素的首地址

2.利用指针将数组作为实参传递给函数

void Test1(int nums[],int n) {
	for (int i = 0; i < n; i++) {
		cout << nums[i] << " ";
	}
}

int main() {
	//利用指针访问数组里面的值
	int nums[] = {1,2,3,4};
	int* p = nums;
	//利用指针将数组作为实参传递给函数
	Test1(nums, 4);//数组名就是数组元素的首地址
	//方法二
	Test1(p, 4);//p指向数组,再将指向数组的指针传给被调函数
}

补充对指针加一 : 例如p++,不会将地址的值加一,而是将数组下移一位

 三.字符指针与字符数组

C语言中,没有字符串类型的变量,通常用字符数组或者字符指针存放字符串

1.字符数组的声明

	char str[] = "C++";
	char str1[] = { 'C','+','+' ,'\0'};
   字符数组的声明方式,在使用第二种声明方式的时候,必须在最后加上\0,此符号代表在这个地方该数组就结束了,如果不加该符号,会访问出界的

  字符数组的访问

	char str[] = "C++";
	char str1[] = { 'C','+','+' ,'\0'};
	printf("%s\n", str1);//%s格式化,表示直接输出整个数组
	printf("%c\n", str[0]);//利用数组法取str第一个字符
	printf("%c\n", *(str1 + 1));//利用指针法取str1的第2个字符

由上面可以看出,可以通过%c一个字符一个字符的输出,那么也可也利用循环输出每个字符

	for (int i = 0; i < strlen(str); i++) {
		printf("%c", str[i]);
	}

还可以用过strlen(str)或者sizeof(str)/sizeof(char)来获取该数组的长度

2.字符指针的声明

char *word = "have a good night";

  字符指针的访问

	const char* word = "have a good night";
	printf("%s\n", word);

	printf("%c\n", word[0]);

	for (int i = 0; i < strlen(word); i++) {
		printf("%c", word[i]);
	}

    对字符指针地址的加减

	const char* word = "have a good night";
	word++;
	printf("%s\n", word);//此时就会将指针下移到第一个a的地方,从a的地方进行输出,
    输出为:ave a good night
	word--;
	printf("%s\n", word);//输出为:have a good night

	const char str[] = "C++";
	str++;//报错,在字符数组中不能进行加减操作,表达式必须是可修改的左值
	printf("%s", str);

四.指针在内存分配中的应用

1.用C语言中的malloc在堆区分配动态内存

1.利用malloc创建一维数组

#include 
#include 
using namespace std;
int main() {
	int n;
	int* p = nullptr;
	cout << "请输入要存放的数据个数:";
	cin >> n;
	//在堆区分配内存
	p = (int *)malloc(n * sizeof(int));
	//输入
	for (int i = 0; i < n; i++) {
		cin >> *(p+i);
	}
	//输出
	for (int i = 0; i < n; i++) {
		cout << *(p+i) << " ";
	}
	free(p);
}

用malloc进行分配,注意malloc的返回值类型位void*类型,需要进行强制类型转换,由于在堆区分配的动态内存,所以在使用完之后,为了防止内存泄漏,需要用free进行释放

2.利用malloc进行开辟二位数组,与释放二位数组

#include 
#include 
using namespace std;
int main() {
	int row = 0 ,col = 0 ;
	int** p = nullptr;
	cout << "依此输入行和列:" << endl;
	cin >> row>>col;
	//在堆区分配内存
	p = (int **)malloc(row * sizeof(int*));
	for (int i = 0; i < row; i++) {
		*(p + i) = (int*)malloc(col * sizeof(int));
	}
	//输入
	for (int i = 0; i < row * col; i++) {
		cin >> p[i / col][i % col];
	}

	//输出
	for (int i = 0; i < row * col; i++) {
		cout << p[i / col][i % col] << " ";
	}
	//释放动态开辟的二维数组
	for (int i = 0; i < row; i++) {
		free(p[i]);
	}
	free(p);
}

原理是,先创建一个指针数组,然后在利用改一级指针去创建动态int类型数组,在输入时候,用到了一个小技巧,用一层循环给二维数组赋值,在使用二维数组时候,应该先将每一层一级指针指向的内存块释放掉,然后再去释放该释放二级指针本身指向的内存块

1.用C++中的new在堆区分配动态内存

用到C++中的new分配内存,用delete释放内存,这里的new和delete只适用于c++中,malloc与free适用于c/c++,直接上例子

1.使用new进行分配一个动态的一维数组

int main() {
	int n;
	cout << "输入数组的大小:";
	cin >> n;
	int* p = new int[n];
	//输入
	for (int i = 0; i < n; i++) {
		cin >> *(p + i);
	}
	//输出
	for (int i = 0; i < n; i++) {
		cout << *(p + i) << " ";
	}

	delete[]p;
}

2.创建二维数组

int main() {
    int rows = 3;
    int cols = 4;

    // 创建动态二维数组
    int** array = new int*[rows];  // 创建一级指针数组

    for (int i = 0; i < rows; i++) {
        array[i] = new int[cols];  // 创建二级指针数组
    }

    // 使用动态二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;  // 给数组元素赋值
        }
    }

    // 打印动态二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            std::cout << array[i][j] << " ";
        }
        std::cout << std::endl;
    }

    // 释放动态二维数组的内存
    for (int i = 0; i < rows; i++) {
        delete[] array[i];  // 释放二级指针数组
    }
    delete[] array;  // 释放一级指针数组

    return 0;
}

和malloc的原理都是一样的,起始可以将一维数组创建的长一点,利用p[i / col][i % col];完全可以将一位数组当二维数组用,二维数组的存储方式也是按行存储的,地址都是连续的

在使用delete时候,数组的释放格式为 delete []p;,单个变量的释放格式为 delete p;

五.使用指针为什么能提高程序的性能

  • 直接访问内存:指针允许直接访问和操作内存中的数据。相比于通过变量的拷贝进行操作,直接访问内存可以减少数据的复制和移动,从而提高程序的执行效率。

#include 
using namespace std;
void Test1(int *p) {
	(*p)++;
}
void Test2(int m) {
	m++;
}
int main() {
	int num1 = 10,num2=10;
	Test1(&num1);
	cout << "Test1 :" << num1 << endl;//输出结果11
	Test2(num2);
	cout << "Test2 :" << num2 << endl;//输出结果10
}

Test1通过直接访问内存,避免了数据的拷贝和移动,提高了程序的执行效率。

Test2函数通过传递值的拷贝来操作变量。当我们调用该函数后,原始变量的值保持不变

  • 减少内存和时间开销:通过传递指针作为函数参数,可以避免在函数调用时产生变量的副本,从而减少内存的使用和传输的时间开销。特别是在处理大型数据结构或大量数据时,使用指针可以显著减少内存和时间的消耗。

  • 动态内存分配:指针使得动态内存分配成为可能,即在程序运行时根据需要分配和释放内存。相比于静态内存分配,动态内存分配可以更灵活地管理内存,避免内存浪费和限制,并且减少了程序启动时的内存占用。

六.指针与const搭配的四种情况

  • const 写在int之前,则限定不能通过*指针去改变该指针指向的值,但是可以指向别的指针
  • const 写在int之后,则限定可以通过*指针去改变该指针指向的值,但是不能指向别的指针
  • 两个const一个写在int前,一个写在变量名前,限制指针不能指向别的指针,并且不允许修改指针指向的值
  • 总结 : 看const离类型(int)近,还是理变量名近,离谁近,就修饰谁,谁就不能变
#include
using namespace std;

int main() {
	int wife = 30;
	int girl = 18;

	//第一种 : 渣男型,普通指针可以随意更改指向与指向地址的值
	int* zha_nan = &wife;
	cout << *zha_nan << endl;

	zha_nan = &girl;
	cout << *zha_nan << endl;

	//第二种 : 直男型,以自我为中心,可以改变指向,但是不可以改变指向的地址的值

	const int* zhi_nan = &wife;
	//*zhi_nan = 25;//报错,不能改变值
	zhi_nan = &girl;
	cout << *zhi_nan << endl;

	//第三种 : 暖男型,专一,不可以改变指向,但是可以改变指向的地址的值

	int* const nuan_nan = &wife;
	//nuan_nan = &girl;//报错,不能改变指向
	*nuan_nan = 25;
	cout << *nuan_nan << endl;

	//第四种 : 超级暖男型,超级专一,不能改变指向,也不能改变指向地址的值

	const int* const _super_nuan_nan = &wife;
	//*_super_nuan_nan = 25;//报错,不能改变指向地址的值
	//super_nuan_nan = &girl;//报错,不能改变指向

	//总结 : const理谁近就修饰谁,理(int)近,则修饰该指针的值不能改变,修饰变量,
	//	    则该指针不能在指向别的变量了
}

七.引用是实现原理

引用的底层也是用指针进行实现的

	int a = 0;
	int b = 2;
	int& c = a;
	c = b;//c是a的别名,将b的值赋值给c,就相当于将b的值也赋值给了a
	cout << c << endl;//2
	cout << a << endl;//2
	//引用变量只能初始化一次,很像 int* const c = a;
    //该指针也只能进行一次初始化,就可以猜测引用的底层也是用该类型指针实现的

在使用引用的时候,编辑器会将引用类型转换为int * const 类型

例子:

void swap1(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}
void swap2(int* const a,int* const b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
int main() {

	int a = 1;
	int b = 2;
	
	swap1(a, b);//引用实现
	cout << "a = " << a << " b = " << b << endl;//a = 2,b =1
	swap2(&a, &b);
	cout << "a = " << a << " b = " << b << endl;//a = 2,b =1
}

对照swap1和swap2,在形参方面,引用变量会被改为int * const类型,在实参方面会将传入整数改为传入地址

八.多级指针

举例二级指针,懂了引用变量的话,那么对二级指针就可以有个优化了,提出指针引用,代码如下:

void home(int * p) {
	int b = 12;
	p = &b;
}
int main() {
	int a = 10;
	int* p = &a;
	//让p存放b的地址
	home(p);
	cout << *p << endl;//10
}

可以看出以上代码是无法改变p的指向的,改变值需要用一级指针,改变一级指针的指向需要用到二级指针,一次类推,这里直接就不写二级指针了,直接用一级指针的引用代替二级指针

void home(int *& p) {
	int b = 12;
	p = &b;
}
int main() {
	int a = 10;
	int* p = &a;
	//让p存放b的地址
	home(p);
	cout << *p << endl;//12
}

由此可以看出,利用指针引用可以将p的指向改变,这也的引用可以增强代码的可读性,与简洁性

九. 指针和函数的关系

1.通过指针使函数返回多个值

比如给你了一个已经初始化的数组,需要定义一个函数,用于返回这个数组的最大值与次大值,这个时候,为了增强代码的可读性,尽量返回一个数组,因为当返回的数据多了,也只能用返回数组实现了,代码如下:

void _max(int *nums,int n,int* exterm) {
	exterm[0] = nums[0];
	exterm[1] = nums[0];

	for (int i = 0; i < n; i++) {
		//大于次大的
		if (nums[i] > exterm[1]) {

			if (nums[i] > exterm[0]) {
				//大于最大的
				exterm[1] = exterm[0];
				exterm[0] = nums[i];
			}

			if (nums[i] < exterm[0]) {
				//小于最大的
				exterm[1] = nums[i];
			}
		}
	}
}

int main() {
	//求该数组中的最大值与次大值
	int nums[] = { 2,6,4,9,5 };
	int exterm[2] = { 0 };
	_max(nums, 5, exterm);

	cout << exterm[0] << " " << exterm[1] << endl;//9,6
}

2.函数指针

函数指针是指向函数的指针变量。它可以存储函数的地址,并允许我们通过该指针调用相应的函数。函数指针在C和C++中都有广泛的应用,可以用于回调函数、函数参数以及实现函数的动态调用等场景。

要理解函数指针,首先需要了解函数的定义和函数指针的声明以及函数指针的使用方式。

1.函数定义:
返回类型 函数名(参数列表) {
    // 函数体
}

2.函数指针的声明:
返回类型 (*指针变量名)(参数列表);

指针变量名:函数指针的名称。
*:用于指明该变量是一个指针。
返回类型:函数指针指向的函数的返回类型。
参数列表:函数指针指向的函数的参数列表。

3.将函数指针指向函数
指针变量名 = 函数名;

4.通过函数指针调用函数:
(*指针变量名)(参数列表);//方法一
(指针变量名)(参数列表);//方法二

举例:

#include 

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    // 声明函数指针
    int (*p)(int, int);

    // 将函数指针指向add函数
    p = add;

    // 通过函数指针调用add函数
    int result = (*p)(10, 5);
    std::cout << "Add: " << result << std::endl;

    // 将函数指针指向subtract函数
    p = subtract;

    // 通过函数指针调用subtract函数
    result = (*p)(10, 5);
    std::cout << "Subtract: " << result << std::endl;

    return 0;
}

3.函数的返回值类型为指针类型

int* creat() {
	return new int(1);//创建一个int类型的变量初始化为1,动态分配的数组不能进行初始化	
}

int main() {

	//函数的返回值类型为指针变量类型,例子,
    //在creat函数里面动态的分配内存,将指向该段内存的指针返回
	int* p = creat();
	cout << "*p :" << *p << endl;//*p : 1

    delete p;
}

十.指针和结构体的关系

利用指针访问结构体里面的值

struct Test
	{
		int a;
		int b;
		int c;
	};
	struct Test ss = { 2,3,4 };
	//声明了结构对象ss,并把ss 的成员初始化为2,3 和4。
	struct Test* ptr = &ss;
	//声明了一个指向结构对象ss 的指针。它的类型是
	//Test *,它指向的类型是Test 。
	printf("%d", ptr->a);
	printf("%d", ptr->b);
	printf("%d", ptr->c);

 

你可能感兴趣的:(c语言,c++,开发语言)