指针实践篇


指针

我们如果想把巧克力寄给远方的朋友,那必须得先知道那个朋友的地址。

程序里的数据就是这个巧克力,远方的朋友就是储存数据的某个地址。

比如,x = 3,3(巧克力)会被寄到 x 的内存地址。

有的时候,并不会自己寄,而是委托别人。我们只是把一份地址寄给了商家,让商家发到朋友手里。

也就是说,我们有俩种寄东西的方法:

  • 直接寄巧克力,寄巧克力;
  • 间接寄巧克力,寄朋友的地址;

像 x = 3 之类的赋值语句,其实只能解决第一种寄法,因为程序的地址也是数字呀,区分不了。

那怎么区分 3 是一个【数据】,还是一个【内存地址】呢?

C语言从语法层面,将储存变量的对象分成了:

  • 基本类型数据;如 char、int、float
  • 地址类型数据;如 char*, int*, float*

这个地址类型数据,就叫【指针】。

  • char a 是基本数据类型;
  • char *a 是地址数据类型;

指针的本质

先做一个思想实验:

#include <stdio.h>
int main(void)
{
   int a = 3;
   // 赋值 3 给 a
   
   printf("%p\n",&a);
   // 输出 a 的地址,假设为 0x0060FEAC
   
   printf("%d\n",*(&a));
   // 取 a 地址的数据
   
	 printf("%x\n",*((int*)0x0060FEAC));
	 // 取 a 地址的数据, 因为变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,在内存里是 0x0060FEAC 这样的数字。
	 // &a == (int*)0x0060FEAC, 也就是说,我们既可以通过【地址助记符】(变量名),也可以直接通过地址
	 // 不过,你得预先知道地址,除了QT之外,别的编译器每次都会改变地址,你其实无法预先知道a的地址,所以通过地址取值的写法,很少见。
	 // 此外,地址也要加上类型,因为不同类型占的内存字节数不一样,类型不同解释也就不同
	 
return 0; 
}

内存地址是指针(或称为指针常量,表示不能变),本质是一个有类型的地址,如 (int*)0x0060FEAC。

指针变量(可以变化的)的本质就是,可以存放 4 字节地址(32位系统),并依据声明类型(如int、char、float)大小寻址的变量,如 type* a。


C语言只有值拷贝

函数的传参,有三类:

  • 传值,实参值的拷贝,因此不能修改实参,而且多创建了一个空间;
  • 传引用,实参地址的拷贝,所以可以修改实参数,可以做输出参数;
  • 传指针,实参地址的拷贝,所以可以修改实参数,可以做输出参数;

传值,如同同名不同人,除了名字相同,俩个人没有什么关联;

传引用,被别人取了绰号,我长得像马云,周围人对着我叫马云的时候,其实就是在叫我,对马云搞事情,就会影响我;

传指针,知道我具体的地址,有这个地址可以找到我,而后在那里搞事情也会影响我。

C语言只有传值、传指针,传引用是C++的。

C语言教科书上说,C语言的函数传参有俩种:

  • 传值是值拷贝;
  • 传指针是地址拷贝。

而我今天在这里说:C语言的函数传参只有一种,就是值拷贝。

值拷贝的概念:把一个变量的值拷贝给另一个变量。

void swap(int a, int b){
    int temp;  
    temp = a;
    a = b;
    b = temp;
}
swap(x, y);
// 赋值等同 int a = x,int b = y;
// 就是把变量 x 的值拷贝给另一个变量a,所以,这就是值拷贝。

void swap(int *p1, int *p2){
    int temp;  
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
swap(&a, &b);
// 赋值等同 int *p1 = a,int *p2 = b;
// 就是把变量 a 的值拷贝给另一个变量p1,所以,这也是值拷贝。

无论是传地址,还是传普通变量,其实函数传参都是值拷贝。

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上…


指针常量与常量指针

例如,

const int *p;

int const *p;

int * const p;

const int * const p;

int const * const p;

看着是有些晕,但好在有一个区别技巧。

首先,明白常量与变量。

const type a 代表 a 是常量(不能改), 如 const char a

type a代表变量(可以改), char a

从右往左读即可,由 C++发明人 提出的。

如, char * const p;

读,p is a const pointer to char.(* 读作 pointer to)

译,p 是一个指向字符的常量指针。(指针指向不能改,但内容可以改)

如,const char *p;

读,p is a pointer to char const. (char const 与 const char 是一个意思)

译,p 是一个指向字符常量的指针变量。(指针内容不能改,但可以改指向)

就有一个规律:

  • const 出现在 * 号右边,指针指向不能改;
  • const 出现在 * 号左边,指针的内容不能改;
  • const 出现在 * 号俩边,指针指向、指针的内容都不能改;
const int *p;
// 读,p is a pointer to int const
// 译,p 是一个指向整型常量的指针,指针内容不能改,但可以改指向,即指针常量。

// 为什么是指针常量呢?
// 这里有一个语言学的规则:偏正关系,语义偏后一个。
// 如牛奶与奶牛,牛奶最后一个是奶,所以本质是奶;奶牛最后一个是牛,所以本质是牛。
// 又如蜜蜂与蜂蜜。
// 指针常量(指针变量+常量): 指针内容不能改,但可以改指向
// 常量指针(常量指针+变量): 指针指向不能改,但内容可以改

int const *p;
// int const 与 const int 相同,所以同上:指针常量。
// 指针内容不能改:*p = 15(错误写法)
// 但可以改指向:p = &x 

int * const p;
// 读,p is a const pointer to int
// 译,p 是一个指向整型变量的常量指针

const int * const p;
// 读,p is a const to pointer to int const
// 译,p 是一个指向整型常量的常量指针(指向、值都不能修改)

int const * const p;
// int const 、const int 相同,所以同上。

指针数组与数组指针

偏正关系,语义偏后一个

指针数组:是存放指针的数组,如 int *p[10]。

数组指针:是指向每行10个元素的二维数组的指针,如 int (*p)[10]。

区别是 指针p 最先和 谁 结合:

  • 先和 [] 结合,是数组;
  • 先和 () 结合,是函数;
  • 先和 * 结合,是指针;

堆上申请二维数组的方法:

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

// 二级指针申请二维数组(返回值版):多个一维指针申请多次,但空间不连续
char **get_memory(int row, int col){

	// 先开辟行
	char **p = (char**)malloc(sizeof(char)*row);
	
	// 再开辟列
	for(int i=0; i<row; i++)
		p[i] = (char*)malloc(sizeof(char*) *col);  
		// *(p+i)等价于p[i]
          
    return p;
}

// 二级指针释放二维数组的方法:
void des_memory( char **p, int row ){
	for(int i=0; i<row; i++)
		free(p[i]);   // 释放列
	free(p);        // 释放行
  p = NULL;       // 防止指针迷途
}

void print( const char**p, int row, int col )
// 加上const,表示指针指向的内容不能修改,保护数据
{
	   for( int i=0; i<row; i++ , putchar(10))
        for( int j=0; j<col; j++ )
            printf("%c ", p[i][j]);
}

int main( )
{
	char **p = NULL;
	p = get_memory(3, 4);
	print(p, 3, 4);
	des_memory(p, 3);  // des_memory释放有些问题,我也不知道为什么
}
#include<stdio.h>
#include<stdlib.h>

// 二级指针申请二维数组(函数传参版):多个一维指针申请多次,但空间不连续
void get_memory(char ** p, int row, int col){

	// 先开辟行
	*p = (char*)malloc(row);
	
	// 再开辟列
	for(int i=0; i<row; i++)
		p[i] = (char*)malloc(sizeof(char *)*col);  
		// *(p+i)等价于p[i]
}

// 二级指针释放二维数组的方法:
void des_memory(char ** p, int row){
	for(int i=0; i<row; i++)
		free(p[i]);   // 释放列
	free(p);        // 释放行
  p = NULL;       // 防止指针迷途
}

void print( const char**p, int row, int col ) // 加上const,表示指针指向的内容不能修改,保护数据
{
	   for( int i=0; i<row; i++ , putchar(10))
        for( int j=0; j<col; j++ )
            printf("%c ", p[i][j]);
}

int main( )
{
	char *p = NULL;
	get_memory(&p, 3, 4);
	print(&p, 3, 4);
	des_memory(&p, 3);  // 这个该怎么释放呢?
}
// 数组指针:二维数组,申请连续的空间,申请简单,释放也简单
void get_memory(char (*p)[3], int row, int col){
	p = ( int(*)[3] )malloc(row * col);
}

Void des_memory(char ** p){
	free(p;)
}

指针函数与函数指针

偏正关系,语义偏后一个

函数指针:是指针,指针存放的变量是地址。

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名类似。

我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,而后通过指针变量就可以找到并调用该函数。(回调函数用的很多)

函数指针的样子,由存放函数的样子决定。

而函数是由:

  • 返回值类型;
  • 参数类型;
  • 个数;

如函数原型 int print(int out) 的指针,应该长这样:

int (*f1) (int) = print; 调用:f1( 传print函数的实参 )

or

typedef int (*F) (int); F f2 = print; 调用:f2( 传print函数的实参 )

指针函数:是函数,函数返回类型的是一个指针。

char *get_memory(int len){
	char *p = (char*)malloc(len);
	return p;                   // 返回一个指针,堆上的内存不会自动清理
}
char *pp = get_memory(1024);  // 获取堆上的内存
发布了124 篇原创文章 · 获赞 362 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_41739364/article/details/105068779