C语言学习笔记10-指针(动态内存分配malloc/calloc、realloc、释放free,可变数组实现;Tips:返回指针的函数使用本地变量有风险!;最后:函数指针)

C语言:指针

1. 指针:保存地址的变量 *p (pointer) ,这种变量的值是内存的地址。

  取地址符& 只用于获取变量(有地址的东西)的地址;scanf函数-取地址符
  地址的大小不一定和int型相同,取决于编译器或系统架构是x86还是x64的,有时需要强转为 long long int; 因此将地址交给整型变量是不靠谱的,我们使用指针类型。 验证方法:  %#x - (int)&i   %p %#p - &i   %lu - sizeof

	int i = 0;
	int p;
	int a[10];
	
	p = (int)&i; //强制类型转换
	printf("&i = %#x ", &i); //类型不匹配,自动转换
	printf("p = %#x\n", p);
	
	printf("real &i = %p\n", &i);
	printf("real &p = %p\n", &p);
	
	//void *    int               x86 x64
	printf("%lu\n", sizeof(&i)); // 4/8
	printf("%lu\n", sizeof(int)); // 4/4
	
	printf("%p\n", &a);
	printf("%p\n", a);
	printf("%p\n", &a[0]);
	printf("%p\n", &a[1]);

在这里插入图片描述

  (补充:scanf函数传入地址时忘加&编译不报错的原因——32位编译器int和指针大小恰好一样,编译器以为传入的int是地址。)

  了解一下——
(1) C语言相邻定义的变量在内存中连续排列(堆栈stack 自顶向下 高地址 - >低地址);
(2) 数组的地址:数组名就是数组首元素地址 arr &arr &arr[0] ;
(3) 一个内存空间大小1B(16进制保存),4GB内存的内存地址编号有 232 个(所以32位机器的内存封顶就是4G,只能编号这么多个);32位系统的一个内存地址编号大小是4B(32位),64位系统的内存地址编号是8B大小(64位)。

2. 指针变量的定义

定义指向: int *p = &i ; 指针p指向一个int,可以将int变量 i 的地址赋给p (不存在 int * 这种类型 – 其实C编译忽略空格)
       int * p, q; 等价于 int *p, q; 均表示定义指针p和整型q (可以做 p = &q;)
       int *p, *q; 这才定义了两个指针变量。

提醒:千万不要定义了指针后还没有指向变量就使用它(即不能用*p)。

3. 指针的运算

char * p = &i[0]
int * q = &j[0]
p+1和q+1分别表示在地址上移动一个sizeof(type) ,在地址层面p加了1(地址升高1),q加了4,就是让指针指向下一个变量。
所以指针如果不是指向一片连续分配的空间(如:数组; i[1] == * (p + 1)),这样的运算毫无意义。
运算 // + - += -= ++ - -
对于 * p ++ :取出p所指的数据,然后把p移向下一个位置(用于数组类的连续空间操作)。【符号优先级 ++ 高于 * 】
  eg:
    for(p = ai; * p != -1; ) {
      printf(“%d\n”, * p++);
    }

两个同类型指针相减(否则非法)的结果是这两个地址间存在几个该类型变量,而不是地址之差。任意的两个指针(不在同一个数组内的)相减和void * 这样纯粹的地址指向指针一样,不能进行减法运算。
不同类型指针不要相互赋值,用int指针给char数组操作会一次性改变4个char变量(因此也不建议强制类型转换,除非你知道自己在干嘛)
void * 俗称万能指针,纯粹访问某个地址,通常在底层编程中使用(控制寄存器或外部设备等)。【 int * p = &i; void * q = (void * ) p; 这没有改变p的变量类型,而是转变不同的眼光看待p所指的变量】

比较 // < <= == >= > !=
比较地址在内存中的大小,数组中的单元的地址一定是线性递增的。

4. 指针变量的作用

运用指针变量,我们在编程时可以通过地址访问变量。低地址不能访问,如存放ascii码的0~255等
  取内容符 * (解引用)用来访问指针所指向的内存地址处的变量,可以做右值也可以做左值。注意与指针定义时的 * 区别,另外它也不是乘法运算。它与 & 互为反作用( * & q 与 & * p 【p是指针,q是普通变量,不能倒过来写,具体参考运算符优先级C语言学习笔记05】 )。
【左值:出现在赋值号左边的不是变量,而是表达式计算的值,是特殊的值(可以接收另一个值),因此称为左值;a[0] = 2; *p = 31; 。】

0地址:机器实际只有1个0地址,操作系统的进程概念会创建虚拟空间,所以每个程序都有自己的0地址。一般这是不能被访问的地址,可以使返回的指针是无效的(初始化指针为0),它被预定义为NULL(C语言中预定义NULL 0),有的编译器使用NULL和0不一定一样。【建议:一般尽量使用NULL】

	int *p, q;
	printf("%p %p\n", p, &q);
	printf("q = %d\n", q); //此时p的地址若是个低地址就禁止访问,若进行访问程序可能崩溃 
	p = &q;
	q = 10;
	printf("%p %p\n", p, &q);
	printf("*p=%d q=%d\n", *p, q);
	printf("%d %p\n", *&q, &*p);

在这里插入图片描述

当指针作为函数参数时,调用函数时的参数赋值必须用实际变量的地址,在函数内用指针就能访问外部实际的变量。

void f(int *p) {
	printf("p = %p\n", p);
	printf("*p = %d\n", *p);
	*p = 31; //左值
	printf("*p' = %d\n", *p);
}
void g(int k) {
	printf("k = %d\n", k);
	printf("&k = %p\n", &k);
	k = 1;
	printf("k' = %d\n", k);
}

int main(int argc, char *argv[])
{	
	int i = 29;
	printf("&i = %p\n", &i);
	printf("i = %d\n", i);
	putchar(10);
	g(i);
	printf("i = %d\n", i);
	f(&i);
	g(i);
	
	return 0;
}

在这里插入图片描述

5. 指针应用场景

(1) 交换两个变量的值。

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

swap(&x, &y);

(2) 函数需要返回多个值,某些值就需要指针带回。
  - 传入函数的指针参数实际上用于保存需要带回的结果

在这里插入图片描述

(3) 函数返回运算状态,结果需要指针返回。
  - 常用套路是让函数返回特殊的不属于有效范围内的值来表示出错:如常用-1或0(文件操作中)表示计算状态,但当任何数值都是有效的可能结果时需要分开返回状态和计算结果。
  - 在C++和java中采用异常机制来解决这个问题。

在这里插入图片描述

6. 指针与数组、const

  C语言学习笔记09-数组中说到:传入函数的数组不能在函数内计算出数组长度——因为传入的是数组首元素的地址,即函数内使用的数组和传入前外面的那个数组是同一个,说明传入函数的数组参数本质是个指针。因此,传入函数的数组参数可以写成 int a[] 也可以写成 int *a ,int * ,int [],它们作为函数原型参数都是等价的。
  数组变量是特殊的指针(const的指针)!数组名和数组首元素地址一样 a == &a[0] —— int a[10]; int *p = a; //不用& 。但是数组的单元表达的是变量,需要用&取地址。因此,指针也能看成是长度为1的数组 p[0] == * p 。 int * const a -> int a[],常量指针一旦得到了某个变量的地址就不能再指向其它变量,所以两个数组间不能直接相互赋值。【具体说明:若 int * const q = &i; 此时q(一个地址)不允许被改变,q++ 是错误的,但是,*q = 26; 是可以的。】
  const修饰指针( const 在 * 之前还是之后 )的差异:const int * p = &i 或 int const * p = &i 表示不能用指针去修改变量 i ,但 i 可以直接修改(i = 10),指针 p 也能重新指向其它的变量(p = &j);只有 * p = 10不被允许。【使用场景:传入函数的结构体参数使用const指针,既能用较少的字节数传递值又能避免函数对外面变量的修改。
  const修饰数组,表示数组的每个单元都是const int型,此时必须通过集成初始化进行赋值。同样为了保护数组传入函数后不被破坏,就可以在函数定义时这样写:int sum(const int a [], int len);

7. 动态内存分配

C99支持用读入变量来定义数组(虽然数组创建后也不能改变大小了),但在ANSI C时期程序员采用动态内存分配的方式解决类似的问题。如下: (malloc函数 头文件stdlib.h)
int * a = (int * )malloc(n * sizeof(int) ); //可申请 4 * n 个字节 malloc返回void * ,所以需要强制转换

  malloc函数与calloc函数比较 ——
  void *malloc(size_t num); malloc参数只有无符号整型的num表示分配的字节数。
  void *calloc(size_t num, size_t size); 两个参数分别是无符号整型num表示分配的对象的个数,以及无符号整型size表示每个对象的大小。【比malloc更好的是calloc会初始化分配的空间,而malloc不行 , 使用如: calloc(n, sizeof(type)) 】

这时,a已经可以被当作数组使用。如:&a[i]。使用结束后记得free(a),动态分配借的要还回去。【free还的只能是借的首地址(谁开始找系统借的),如果不是程序会报错】

7.1. 查看本机剩余最大可用内存空间:(如果没有空间了,malloc申请失败返回0,也可以说是NULL)

在这里插入图片描述
如果上面while中的判断条件未加(),会提示以下错误:
[Warning] suggest parentheses around assignment used as truth value [-Wparentheses]
这是因为编译器生怕你在编程时搞混了赋值和比较,因此希望你明确自己要做的操作,改成图片中的样子就ok了。

7.2. 接收不定长字符串:(malloc、realloc、free)
	int cnt = 0, len = 20;
	char ch;
	char *s1 = (char*)malloc(len);
	char *s2 = NULL; //出于安全性优化考虑,添加*s2
	
	if (s1 == NULL) {
		return; //直接返回被调用点
	}
	while ((ch = getchar()) != EOF) { //windows ctrl+Z    linux ctrl+D  关闭程序ctrl+C
		s1[cnt++] = ch; //s1[cnt] = ch;cnt++;可合并成s1[cnt++] = ch;
		if ( cnt >= len ) {
			len += 20; // *= 2
			s2 = (char*)realloc(s1, len);
			if ( s2 == NULL ) {
				free(s1);
				return;
			}
			else s1 = s2;
		}
	}
	s1[cnt] = '\0'; //如果不加本行,输出末尾会多出符号直至遍历到存放 0 / '\0'的内存单元
	puts(s1);
	free(s1); //切记释放内存
	free(s2);

在这里插入图片描述

动态申请内存空间后,记住合适的时机释放它,虽然一般初学编程时程序小,运行结束后操作系统会帮你善后处理,但是不释放这个习惯如果保留到较大的程序编程中就会有问题。

7.3. 实现可变数组【自定义数组结构】:(memcpy string):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const int BLOCK_SIZE = 20; //每20个单元一个区

//定义自己的数组结构,实现可变数组
//可变数组缺陷:每次需要申请更大的线性连续空间,不够高效
typedef struct {
	int *array;
	int size;
} Array; //此处如果改成指针类型*Array 弊端在于想要创建一个本地Array变量就做不出来了,且易造成误解

Array array_create(int init_size); //创建数组
void array_free(Array *a); //释放数组空间中的*array
int array_size(const Array *a); //计算数组可用单元数
int* array_at(Array *a, int index); //用于访问数组某个单元(左值、右值)
int array_get(const Array *a, int index);
void array_set(Array *a, int index, int value);
void array_inflate(Array *a, int more_size); //可变数组核心函数:使数组变大

/*
* ****** Main ******
*/
int main(int argc, const char *argv[]) 
{
	int size = 20; //100
	Array a = array_create(size);
	
//	printf("array a's size: %d\n", array_size(&a));
//	*array_at(&a, 0) = 123; //array_set(&a, 0, 321);
//	printf("%d\n", *array_at(&a, 0)); //printf("%d\n", array_get(&a, 0));
	
	int number = 0;
	int cnt = 0;
	//输入数等于-1时表示退出
	while ( number != -1 ) {
		//scanf("%d", array_at(&a, cnt++));
		scanf("%d", &number);
		if ( number != -1 ) {
			*array_at(&a, cnt++) = number;
		}
	}
	
	printf("array a's size: %d\n", array_size(&a));
	for ( cnt -= 1; cnt >= 0; cnt-- ) {
		printf("%d\t", *array_at(&a, cnt));
	}
	
	array_free(&a);
	
	return 0;
}

//typedef struct {
//	int *array;
//	int size;
//} Array;

Array array_create(int init_size) //Array* array_create(Array *a, int init_size) a->size
{
	Array a;
	a.size = init_size;
	a.array = (int*)malloc(sizeof(int) * a.size);
	
	//返回本地Array类型变量可以让外部的调用者更灵活地做计算,若用指针会很麻烦
	//如需要判断指针是否null,是否之前创建过了(需要先free)
	return a;
}

void array_free(Array *a) 
{
	free(a->array);
	//再加两重保险,防止外部free后再次调用
	a->array = NULL;
	a->size = 0;
}

//封装,隐藏内部细节,安全性高
int array_size(const Array *a) 
{
	return a->size;
}

int* array_at(Array *a, int index) 
{
	if ( index >= a->size ) { //index < 0
		//array_inflate(a, index - a->size + 1); //不经济
		array_inflate(a, (index/BLOCK_SIZE + 1) * BLOCK_SIZE - a->size); //不一定只增加1个block
	}
	
	//返回指针而不是值,可以让外部调用者将其加*当做左值使用
	return &(a->array[index]); //优先级搞不清了,工程中通常直接加括号
}
//另一种方式:避免 *函数
int array_get(const Array *a, int index) 
{
	return a->array[index];
}
void array_set(Array *a, int index, int value) //修改
{
	a->array[index] = value;
}

void array_inflate(Array *a, int more_size) //理论上数组不能自增长,如何实现可变数组
{
	//申请一块新的空间
	int *p = (int*)malloc(sizeof(int) * (a->size + more_size));
//	int i;
//	for ( i=0; i < a->size; i++ ) {
//		p[i] = a->array[i];
//	} //memcpy(dst, src, nbytes) //string.h
	memcpy(p, a->array, sizeof(int) * a->size);
	free(a->array);
	a->array = p;
	a->size += more_size;
}

8. 返回指针的函数 —— (返回传入的指针)

返回本地变量的地址是危险的。因为离开函数后本地变量不存在了,这个本地变量的地址可能会被重新分配给其它变量使用:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,p指向的地址变化了,有时甚至程序会崩溃!返回指针的函数,使用全局变量或静态变量相对安全(线程不安全,函数不可重用)。【注意不要使用全局变量在函数间来回传递参数和结果】
最好的做法是返回传入的指针:在主函数里建一个指针给其它函数,其他函数回传指针。

另外看一种特殊的指针 —— 函数指针

函数名也代表一个地址,指针用来存放地址,函数类型的指针可以有:
在这里插入图片描述
在这里插入图片描述

函数指针能做什么?请看:
在这里插入图片描述
在这里插入图片描述

更主要是用于
(1)接受用户输入后选择该干什么事(调用哪一个函数),可以比switch更简洁。

#include <stdio.h>

void f(int i) {
	printf("in f(), %d\n", i);
}

void g(int i) {
	printf("in g(), %d\n", i);
}

void h(int i) {
	printf("in h(), %d\n", i);
}

void k(int i) {
	printf("in k(), %d\n", i);
}

/* *** */
int main()
{
	int i = 0;
	scanf("%d", &i);
	
	void (*fa[])(int) = {f,g,h,k}; //函数指针数组集合初始化
	
	if ( i >= 0 && i < sizeof(fa)/sizeof(fa[0]) ) {
		(*fa[i])(0);
	}
	
//	switch ( i ) {
//		case 0: f(0);break;
//		case 1: g(0);break;
//		case 2: h(0);break;
//	}
	
//	if ( i == 0 ) {
//		f(0);
//	}
//	else if ( i == 1 ) {
//		g(0);
//	}
	
}

在这里插入图片描述

(2)向函数中传入函数:

#include <stdio.h>

int plus(int a, int b) {
	return a + b;
}
int minus(int a, int b) {
	return a - b;
}
void cal(int (*f)(int, int)) {
	printf("%d\n", (*f)(2,3) );
}

/* *** */
int main()
{
	cal(plus);
	cal(minus);

}

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值