C语言程序设计学习笔记:P9-指针_以下哪个打印指针地址的写法是正确的-程序员宅基地

技术标签: c语言  编程语言  数组  C语言程序设计  指针  软件开发  

本系列文章为浙江大学翁恺C语言程序设计学习笔记,前面的系列文章链接如下
C语言程序设计学习笔记:P1-程序设计与C语言
C语言程序设计学习笔记:P2-计算
C语言程序设计学习笔记:P3-判断
C语言程序设计学习笔记:P4-循环
C语言程序设计学习笔记:P5-循环控制
C语言程序设计学习笔记:P6-数据类型
C语言程序设计学习笔记:P7-函数
C语言程序设计学习笔记:P8-数组


一、指针

1.1 取地址运算

运算符&就是取地址运算符,我们在第一篇博客就看到了它:scanf(“%d”, &i);。里的&的作用就是获得变量的地址,它的操作数必须是变量。为什么变量有地址?因为C语言的变量是放在内存里的,比如一个int类型的变量在内存里面要占据4个字节。 那这个地址是个什么样的值呢?我们知道地址这些东西用16进制表达比较方便,我们来看看变量i的地址。

#include <stdio.h>

int main(void)
{
    
	int i = 0;
	printf("0x%x\n", &i);
	return 0;
}

运行,可以看出32位和64位编译器结果如下:
在这里插入图片描述
在这里插入图片描述


我们可以看出这些变量的地址很像个整数,我们来看看将地址赋值给一个整数看看会有什么后果。

#include <stdio.h>

int main(void)
{
    
	int i = 0;
	int p = &i;
	printf("0x%x\n", &i);
	printf("%p\n", &i);
	return 0;
}

可以看出有warning,提示我们int与int*类型不同。
在这里插入图片描述


如果现在我将地址强制转换成int,然后将这个int变量以16进制打印出来,看看会有什么结果。

#include <stdio.h>

int main(void)
{
    
	int i = 0;
	int p;
	p = (int)&i;
	printf("0x%X\n", p);
	printf("%p\n", &i);
	return 0;
}

32位编译器时,输出一样。
在这里插入图片描述
64位编译器时,作为int和作为地址输出就不一样了。
在这里插入图片描述


为了探索为什么不相等,我们写代码看下int和地址的大小分别是多少:

#include <stdio.h>

int main(void)
{
    
	int i = 0;
	int p;
	p = (int)&i;
	printf("0x%X\n", p);
	printf("%p\n", &i);
	printf("%lu\n",sizeof(int));
	printf("%lu\n", sizeof(&i));
	return 0;
}

可以看出64位编译器下,int大小是4个字节,这个地址取出来的大小是8个字节。
在这里插入图片描述
可以看出32位编译器下,int大小是4个字节,这个地址取出来的大小也是4个字节。
在这里插入图片描述
因此,地址的大小是否与int相同取决于编译器。我们使用printf打印地址时,应该使用%p。地址和整数的大小并不永远相等的,这和你的架构有关。


&必须接变量,不能对没有地址的东西取地址,比如:

• &(a+b)
• &(a++)
• &(++a)

我们测试一下:

#include <stdio.h>

int main(void)
{
    
	int i = 0;
	printf("%p",&(i++));

	return 0;
}

运行,结果如下:
在这里插入图片描述


我们试下&可不可以取以下东西的地址。

• 变量的地址
• 相邻的变量的地址
• &的结果的sizeof
• 数组的地址
• 数组单元的地址
• 相邻的数组单元的地址

我们来测试相邻变量的地址:可以看出两个变量放在一起,位置相差sizeof(int)=4个字节。
在这里插入图片描述
这是因为变量都放在堆栈里面,而根据C语言内存模型可以知道,在堆栈里面分配内存是自顶向下。所以我们先写的变量i的地址更高,后写的变量p地址更低,但它们是紧挨着的。它们的差距就是4,这个4就是sizeof(int)。
在这里插入图片描述


我们再来看看对数组取地址:

#include <stdio.h>

int main(void)
{
    
	int a[10];

	printf("%p\n", &a);
	printf("%p\n", a);
	printf("%p\n", &a[0]);
	printf("%p\n", &a[1]);

	return 0;
}

可以看出 &a = a = &a[0]
在这里插入图片描述

1.2 指针

如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量? 答案是肯定的,比如我们用过的scanf函数,scanf(“%d”, &i);。根据scanf()的原型,我们需要一个参数能保存别的变量的地址。如何表达能够保存地址的变量?这时候就需要指针。

普通变量的值是实际的值,指针就是保存地址的变量,指针变量的值是具有实际值的变量的地址,示意图如下:
在这里插入图片描述
指针的用法如下

int i;
int* p = &i;
int* p,q;  //定义了一个指针p和一个int变量q。我们不是把*加给了int,而是把*加给了p。
int *p,q;  //和上面那个一样的意思。
如果要定义两个指针,那就是int *p,*q

指针可以作为作为参数

void f(int *p);
在被调用的时候得到了某个变量的地址:
int i=0; f(&i);
在函数里面可以通过这个指针访问外面的这个i

我们通过一个例子来进行测试,看是否能在函数里面读取到外面变量的地址。

#include <stdio.h>

void f(int *p);

int main(void)
{
    
	int i = 6;
	printf("&i=%p\n", &i);
	f(&i);

	return 0;
}

void f(int *p)
{
    
	printf(" p=%p\n", p);
}

运行,可以看出在函数内部也能得到变量i的地址。
在这里插入图片描述


访问那个地址上的值
我通过指针读到变量的地址后,如果想改变那个变量的值,该怎么办呢?这时候需要使用 *,它是一个单目运算符,用来访问指针的值所表示的地址上的变量。

• 可以做右值也可以做左值
• int k = *p;
• *p = k+1;

我们来测试一下是否可以通过*访问到指针表示的地址上的变量。

#include <stdio.h>

void f(int *p);

int main(void)
{
    
	int i = 6;
	printf("&i=%p\n", &i);
	printf("i=%d\n", i);
	f(&i);

	return 0;
}

void f(int *p)
{
    
	printf(" p=%p\n", p);
	printf(" *p=%d\n", *p);
}

运行,可以发现成功访问到了。
在这里插入图片描述
那我们能不能改变那个变量的值呢?

#include <stdio.h>

void f(int *p);

int main(void)
{
    
	int i = 6;
	printf("&i=%p\n", &i);
	printf("i=%d\n", i);
	f(&i);
	printf("i=%d\n", i);

	return 0;
}

void f(int *p)
{
    
	printf(" p=%p\n", p);
	printf(" *p=%d\n", *p);
	*p = 26;
}

运行,可以发现i的值被改变了。
在这里插入图片描述

左值之所以叫左值

• 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
• a[0] = 2;
• *p = 3;
• 是特殊的值,所以叫做左值

指针的运算符&和*

• 他们互相反作用
• *&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr
• &*yptr -> &(*yptr) -> &(y) -> 得到y的地址,也就是yptr -> yptr

为什么int i; scanf(“%d”, i);编译没有报错?
因为i是个整数,且刚好在32位架构下,整数和地址一样大。你把整数传进去和把地址传进去,scanf没看出区别。


1.3 指针的使用

指针的应用场景1:交换两个变量的值

#include <stdio.h>

void swap(int *pa, int *pb);

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

	return 0;
}

void swap(int *pa, int *pb)
{
    
	int t = *pa;
	*pa = *pb;
	*pb = t;
}

运行,可以看出a和b的值成功交换了。
在这里插入图片描述


指针应用场景2:函数返回多个值,某些值就只能通过指针返回。比如传入的参数实际上是需要保存带回的结果的变量。

现在我有一个数组,需要返回最大值和最小值。我可以使用两个变量保存最大值与最小值,并将两个变量的地址作为参数传入函数。在函数中通过指针访问那两个变量并修改它们的值。

#include <stdio.h>

void minmax(int a[], int len, int *max, int *min);

int main(void)
{
    
	int a[] = {
    1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
	int min,max;
	minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
	printf("min=%d,max=%d\n", min, max);

	return 0;
}

void minmax(int a[], int len, int *min, int *max)
{
    
	int i;
	*min = *max = a[0];
	for (i = 1; i < len; i++) {
    
		if (a[i] < *min) {
    
			*min = a[i];
		}
		if (a[i] > *max) {
    
			*max = a[i];
		}
	}
}

运行,可以发现成功找到了最大值与最小值。
在这里插入图片描述


指针应用场景3:函数返回运算的状态,结果通过指针返回

• 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
  • -1或0(在文件操作会看到大量的例子)
• 但是当任何数值都是有效的可能结果时,就得分开返回了
  • 状态用函数的return来返回,实际的值通过指针参数来返回
  • 后续的语言(C++,Java)采用了异常机制来解决这个问题

举例:两个整数做除法

#include <stdio.h>

int divide(int a, int b, int *result);

int main(void)
{
    
	int a=5;
	int b=2;
	int c;
	if (divide(a, b, &c)) {
    
		printf("%d/%d=%d\n",a,b,c);
	}

	return 0;
}

int divide(int a, int b, int *result)
{
    
	int ret = 1;
	if (b == 0) ret = 0;
	else {
    
		*result = a/b;
	}
	return ret;
}

可以看出,如果无法相除,就会返回0。如果成功,就会返回1,且结果通过指针保存在c中。
在这里插入图片描述


指针最常见的错误:定义了指针变量,还没有指向任何变量,就开始使用指针
我这里定义了一个指针,现在试着让它指向的值更改为12。

#include <stdio.h>

int main(void)
{
    
	int i = 6;
	int *p;
	int k = 12;
	*p = 12;

	return 0;
}

运行,可以看出报错。
在这里插入图片描述

我们来分析原因:我们知道,所有的本地变量都不会有默认的初始值,如果没有对它做过赋值,这个p里面没有明确的值,可能是个乱七八糟的东西。如果把它当作地址的话,可能会指向一片莫名其妙的地方。当你使用*p=12时,你试图向那个地方写入12,而这个地方可能是不能写的。
在这里插入图片描述
我们来测试下,如果让 *p有个初始值为0,这个地方是肯定不能写的,现在让它写入12看看。可以看出直接中断。
在这里插入图片描述

1.4 指针与数组

当我们向函数传一个普通变量,参数接收到的是个值。如果传一个指针,参数也是接收到的一个值,这个时候的值是地址。当我们把数组作为一个值传递给函数,函数的参数表内有一个变量去接受那个数组,这个变量到底接收到了什么?我们拿上面写的minmax这个函数来做实验。

为什么在minmax函数内不能用sizeof算出这个a[]的元素个数呢? 到底它的sizeof是多少呢?我们在main函数和minmax函数中分别取打印a[]的sizeof。同时打印a[]的地址。

	#include <stdio.h>

	void minmax(int a[], int len, int *max, int *min);

	int main(void)
	{
    
		int a[] = {
     1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55 };
		int min, max;
		printf("main sizeof(a)=%lu\n", sizeof(a));
		printf("main a=%p\n", a);
		minmax(a, sizeof(a) / sizeof(a[0]), &min, &max);
		printf("min=%d,max=%d\n", min, max);

		return 0;
	}

	void minmax(int a[], int len, int *min, int *max)
	{
    
		int i;
		printf("minmax sizeof(a)=%lu\n", sizeof(a));
		printf("minmax a=%p\n", a);
		*min = *max = a[0];
		for (i = 1; i < len; i++) {
    
			if (a[i] < *min) {
    
				*min = a[i];
			}
			if (a[i] > *max) {
    
				*max = a[i];
			}
		}
	}

运行,可以看出在32位编译器条件下,minmax函数中a[]的大小为4,刚好为一个指针的大小。同时,minmax函数与main函数中a[]的地址一模一样,说明这两个完全是同一个数组,相当于一个演员演了一对双胞胎。
在这里插入图片描述
为了来测试这两个数组就是同一个,我们在minmax函数中将a[0]的值更改为1000,然后去main函数看看a[0]的值。我们运行程序,可以看出a[0[=1000。
在这里插入图片描述
所以,函数参数表里面的数组就是指针!这也就解释了为什么在参数表内不要在a[]的括号内写数字,为什么在函数内无法用sizeof求出a[]的大小。


既然参数表里的数组就是个指针,那我们在参数表内把它写成个指针,行不行?答案是可以的。
在这里插入图片描述


可以看出,我们虽然传入了一个指针,但是对它做了一系列数组的操作。看起来数组和指针似乎存在某种联系,我们有以下总结:
1、传入函数的数组成了什么?

函数参数表中的数组实际上是指针
sizeof(a) == sizeof(int*)
但是可以用数组的运算符[]进行运算

2、数组参数

以下四种函数原型是等价的:
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

3、数组变量是特殊的指针

数组变量本⾝身表达地址,所以
  int a[10]; int*p=a; // 无需用&取地址
  但是数组的单元表达的是变量,需要用&取地址
  a == &a[0]
[]运算符可以对数组做,也可以对指针做:
  p[0] <==> a[0]
*运算符可以对指针做,也可以对数组做:
  *a = 25;
  *p = a[0] = 25
数组变量是const的指针,所以不能被赋值
  int b[] = a;  //不可以
  int *q = a;  //可以
  int a[] <==> int * const a  这个const加在这里告诉这个a是个常数不能被改变

1.4 指针与const

const是个修饰符,加在变量前面,告诉这个变量不能被修改。指针是一种变量,由两部分内容组成:一个是指针本身,一个是指针所指的那个变量。那么在这种情况下,当指针与const遇到了,指针本身可以是const,指针指向的那个值可以是const。他们有什么样的区别和联系呢?

1、指针是const

表示一旦得到了某个变量的地址,不能再指向其他变量
int * const q = &i;  // q这个指针是 const,即q的值(i的地址)不能被改变,q指向i这个事实不能改变了。
*q = 26;             // OK,通过q做一些访问并改变i这个变量的值是可以的
q++;                 // ERROR

2、所指是const

表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
const int *p = &i;
*p = 26; // ERROR! (*p) 是 const
i = 26; //OK
p = & j; //OK

3、这里有三种写法,看看什么意思

int i;
const int* p1 = &i; //不能通过*p1去修改i的值
int const* p2 = &i; //不能通过*p2去修改i的值
int *const p3 = &i; //p3必须指向i,不能指向其他地方。

判断哪个被const了的标志是const在*的前面还是后面。const在后面,指针不能修改。const在前面,不能通过指针修改值。


4、转换
我们总是可以把一个非const的值转换成const的。

void f(const int* x); //代表你给我一个指针,我在我的函数内部不会去动这个指针。
int a = 15;
f(&a);  // ok,函数需要const int的指针,我们给了1个非const的指针,没问题。
const int b = a;

f(&b); // ok
b = a + 1; // Error!

这种我们拿来做什么呢?当要传递的参数的类型比地址大的时候(传一些结构体之类的),这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改。


5、const数组
数组也是一种特殊的指针,因此也可以搭配const使用。

const int a[] = {
    1,2,3,4,5,6,};
数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
所以必须通过初始化进行赋值

6、保护数组值

• 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
• 为了保护数组不被函数破坏,可以设置参数为const
  • int sum(const int a[], int length);

小测验

1、对于:

int a[] = {5, 15, 34, 54, 14, 2, 52, 72};
int *p = &a[5];
则p[-2]的值是?
A. 编译出错,因为数组下标越界了
B. 运行出错,因为数组下标越界了
C. 54
D. 2

答案:C

2、如果:

int a[] = {0};
int *p = a;
则以下哪些表达式的结果为真?
A. p == a[0]
B. p == &a[0]
C. *p == a[0]
D. p[0] == a[0]

答案:B、C、D

3、以下变量定义:

int* p,q;
中,p和q都是指针。

正确答案:错误

4、对于:

int a[] = {5, 15, 34, 54, 14, 2, 52, 72};
int *p = &a[1];
则p[2]的值是?

答案:54

二、指针运算

2.1 指针运算

我们都知道1+1=2。但是,对于指针呢?让指针加1,结果是真正加1了吗?我们来测试一下。

#include <stdio.h>
int main(void)
{
    
	char ac[] = {
    1,2,3,4,5,6,7,8,9};
	char *p = ac;
	printf("p  =%p\n", p);
	printf("p+1=%p\n", p+1);
	return 0;
}

运行,可以看出相差1。
在这里插入图片描述
那我们把char类型的数组更改为int类型的呢?可以看出相差4。
在这里插入图片描述
sizeof(char)=1,sizeof(int)=4。因此,指针加1不是让地址值加1,而是在地址值上加1个sizeof(所指向值的类型)。示意图如下所示:
在这里插入图片描述


因此,对于1+1这个问题:

• 给一个指针加1表示要让指针指向下一个变量
int a[10];
int *p = a;
*(p+1) —> a[1]
*(p+n) —> a[n]
• 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义

指针计算

• 这些算术运算可以对指针做:
  • 给指针加、减一个整数(+, +=, -, -=)
  • 递增递减(++/--)
  • 两个指针相减

我们来看看指针相减是怎么一回事。我们分别定义一个char类型的数组和int类型的数组,用两个指针变量保存第一个元素的地址和第6个元素的地址。接着让这两个指针相减,看结果是多少。

#include <stdio.h>

int main(void)
{
    
	char ac[] = {
    1,2,3,4,5,6,7,8,9};
	char *p = &ac[0];
	char *p1 = &ac[5];
	printf("p  =%p\n", p);
	printf("p1 =%p\n", p1);
	printf("p1-p=%d\n", p1-p);

	int ai[] = {
    1,2,3,4,5,6,7,8,9 };
	int *q = &ai[0];
	int *q1 = &ai[6];
	printf("q  =%p\n", q);
	printf("q1 =%p\n", q1);
	printf("q1-q=%d\n", q1 - q);

	return 0;
}

可以看出,两个数组相减不是得到两个地址的差值,而是还要去除以类型的大小,表示这段区域可以放多少个这样的值。
在这里插入图片描述


我们在程序里面经常看到*p++这个东西

• 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
• *的优先级虽然高,但是没有++高
• 常用于数组类的连续空间操作
• 在某些CPU上,这可以直接被翻译成一条汇编指令

我们写一些代码来看看*p++的用法。

#include <stdio.h>

int main(void)
{
    
	char ac[] = {
    1,2,3,4,5,6,7,8,9, -1};
	char *p = ac;
	int i;
	//原始的方法遍历数组
	for (i = 0; i < sizeof(ac) / sizeof(ac[0]); i++) {
    
		printf("%d ", ac[i]);
	}
	printf("\n");

	//使用*p++的方法遍历数组
	while (*p != -1) {
    
		printf("%d ", *p++);
	}
	printf("\n");

	return 0;
}

指针比较

<, <=, ==, >, >=, != 都可以对指针做
比较它们在内存中的地址
数组中的单元的地址肯定是线性递增的

0地址
现在的操作系统都是多进程的操作系统,它的基本管理单元叫做进程。什么是进程?比如你运行了一个浏览器,那就是个进程。我们打开了Visual Studio进行编程,Visual Studio也是个进程。操作系统会给每个进程一些虚拟的空间,所有的程序在运行时都以为自己有一片从0开始的连续的空间。因此,任何程序都有0地址,但是这个0地址不能碰,有的甚至都不能读。

• 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
• 所以你的指针不应该具有0值
• 因此可以用0地址来表示特殊的事情:
  • 返回的指针是无效的
  • 指针没有被真正初始化(先初始化为0)
• NULL是一个预定义的符号,表示0地址
  • 有的编译器不愿意你用0来表示0地址

指针的类型
指针是有类型的,不同类型的指针不能互相赋值。我们来举个例子,有个char类型的指针和int类型的指针,现在将char类型指针赋值给int类型的指针,看会有什么后果。

#include <stdio.h>
int main(void)
{
    
	char ac[] = {
    1,2,3,4,5,6,7,8,9,-1};
	char *p = ac;
	int  ai[] = {
    1,2,3,4,5,6,7,8,9,-1};
	int  *q = ai;
	q=p;
	return 0;
}

运行,可以看出不报错,但是有warning。
在这里插入图片描述
这样做会导致一些不好的后果。如果我将指针p赋值给q,现在我让*q=0,按理说应该把0赋值给ac[0]。但实际上会让ac[0]、ac[1]、ac[2]、ac[3]都赋值为0。
在这里插入图片描述

指针赋值总结如下

• 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
• 但是指向不同类型的指针是不能直接互相赋值的
• 这是为了避免用错指针

指针的类型转换
如果我就想做指针类型转换怎么办呢?实际上是可以做的,不过不要乱用,一般在malloc的时候搭配void*使用。

• void* 表示不知道指向什么东西的指针
  • 计算时与char*相同(但不相通)
• 指针也可以转换类型
  • int *p = &i; void*q = (void*)p;
• 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量
  • 我不再当你是int啦,我认为你就是个void!

总结:指针的作用

用指针来做什么
• 需要传入较大的数据时用作参数
• 传入数组后对数组做操作
• 函数返回不止一个结果
• 需要用函数来修改不止一个变量
• 动态申请的内存...

2.2 动态内存分配

之前我们讲过,如果输入数据时,先告诉你个数,然后再输入,要记录每个数据。C99可以用变量做数组定义的大小,C99之前呢?那就只能用动态内存分配,如下面一行所示:

int *a = (int*)malloc(n*sizeof(int));

我们来看看malloc的定义:

• 使用前需要包含stdlib.h这个头文件:  #include <stdlib.h>
• malloc函数原型为: void* malloc(size_t size);
• 向malloc申请的空间的大小是以字节为单位的
• 返回的结果是void*,需要类型转换为自己需要的类型
• (int*)malloc(n*sizeof(int))

我们写出代码来看看malloc如何工作的:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    
	int number;
	int *a;
	int i;
	printf("输入数量:");
	scanf_s("%d", &number);

	//申请一片number*sizeof(int)个字节的内存,然后进行类型转换
	//现在我们可以将a当作int类型的数组使用了
	a=(int*)malloc(number*sizeof(int));
	for (i = 0; i < number; i++) {
    
		scanf_s("%d", &a[i]);
	}
	for (i = number - 1; i >= 0; i--) {
    
		printf("%d ", a[i]);
	}
	//这片内存是借的,用完需要还
	free(a);

	return 0;
}

运行,可以看到成功分配了内存,并倒序打印了数组中的值。
在这里插入图片描述


如果malloc申请时没空间了怎么办?如果申请失败则返回0,或者叫做NULL。你的系统能给你多大的空间?我们写出代码来看下。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    
	void *p;
	int cnt = 0;
	while ((p = malloc(100 * 1024 * 1024))) {
    
		cnt++;
	}
	printf("分配了%d00MB的空间\n", cnt);

	return 0;
}

在32位编译平台下,分配了1900MB。
在这里插入图片描述
在64位编译平台下,分配内存45G。
在这里插入图片描述


free()

• 把申请得来的空间还给“系统”
• 申请过的空间,最终都应该要还
  • 混出来的,迟早都是要还的
• 只能还申请来的空间的首地址
• free(0)?

我们申请一段内存,然后将首地址++,并释放该地址,看会发生什么。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    
	char *p;
	int cnt = 0;
	p = malloc(100 * 1024 * 1024);
	p++; 
	free(p);

	return 0;
}

可以看出,直接抛出异常。
在这里插入图片描述
我们再来看看释放一个不是申请来的内存看看有什么结果。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    
	int *p;
	int i;
	p = &i;
	free(p);

	return 0;
}

可以看出也抛出异常。
在这里插入图片描述

如果我们free(NULL)不会出错。
在这里插入图片描述
这是因为NULL就是0地址,0地址不可能是个有效的地址,它不可能是malloc来的。free也是一个函数,如果给它一个NULL,那它就不做事情。只是,有什么必要做这件事情呢?这是因为良好习惯就是:有一个指针出来了,我们先初始化为0。如果由于某些原因我们没有去malloc分配一片内存给它或者malloc得到一个失败的结果,我们去free那个指针没问题。


常见问题

• 申请了没free—>长时间运行内存逐渐下降
  • 新手:忘了
  • 老手:找不到合适的free的时机
  如果程序小,基本没影响,程序结束后内存被释放。如果程序很大,就会造成严重后果。
• free过了再free
• 地址变过了,直接去free

小测验

1、对于以下代码段,正确的说法是:

char *p;
while (1) {
    
    p = malloc(1);
    *p = 0;
}

A. 最终程序会因为没有没有空间了而退出
B. 最终程序会因为向0地址写入而退出
C. 程序会一直运行下去
D. 程序不能被编译

答案:B

2、对于以下代码段:

int a[] = {
    1,2,3,4,5,};
int *p = a;
int *q = &a[5];
printf("%d", q-p);sizeof(int)4时,以下说法正确的是:

A. 因为第三行的错误不能编译
B. 因为第三行的错误运行时崩溃
C. 输出5
D. 输出20

答案:C

3、使用malloc就可以做出运行时可以随时改变大小的数组
答案:错误

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/InnerPeaceHQ/article/details/121546043

智能推荐

软件测试流程包括哪些内容?测试方法有哪些?_测试过程管理中包含哪些过程-程序员宅基地

文章浏览阅读2.9k次,点赞8次,收藏14次。测试主要做什么?这完全都体现在测试流程中,同时测试流程是面试问题中出现频率最高的,这不仅是因为测试流程很重要,而是在面试过程中这短短的半小时到一个小时的时间,通过测试流程就可以判断出应聘者是否合适,故在测试流程中包含了测试工作的核心内容,例如需求分析,测试用例的设计,测试执行,缺陷等重要的过程。..._测试过程管理中包含哪些过程

政府数字化政务的人工智能与机器学习应用:如何提高政府工作效率-程序员宅基地

文章浏览阅读870次,点赞16次,收藏19次。1.背景介绍政府数字化政务是指政府利用数字技术、互联网、大数据、人工智能等新技术手段,对政府政务进行数字化改革,提高政府工作效率,提升政府服务质量的过程。随着人工智能(AI)和机器学习(ML)技术的快速发展,政府数字化政务中的人工智能与机器学习应用也逐渐成为政府改革的重要内容。政府数字化政务的人工智能与机器学习应用涉及多个领域,包括政策决策、政府服务、公共安全、社会治理等。在这些领域,人工...

ssm+mysql+微信小程序考研刷题平台_mysql刷题软件-程序员宅基地

文章浏览阅读219次,点赞2次,收藏4次。系统主要的用户为用户、管理员,他们的具体权限如下:用户:用户登录后可以对管理员上传的学习视频进行学习。用户可以选择题型进行练习。用户选择小程序提供的考研科目进行相关训练。用户可以进行水平测试,并且查看相关成绩用户可以进行错题集的整理管理员:管理员登录后可管理个人基本信息管理员登录后可管理个人基本信息管理员可以上传、发布考研的相关例题及其分析,并对题型进行管理管理员可以进行查看、搜索考研题目及错题情况。_mysql刷题软件

根据java代码描绘uml类图_Myeclipse8.5下JAVA代码导成UML类图-程序员宅基地

文章浏览阅读1.4k次。myelipse里有UML1和UML2两种方式,UML2功能更强大,但是两者生成过程差别不大1.建立Test工程,如下图,uml包存放uml类图package com.zz.domain;public class User {private int id;private String name;public int getId() {return id;}public void setId(int..._根据以下java代码画出类图

Flume自定义拦截器-程序员宅基地

文章浏览阅读174次。需求:一个topic包含很多个表信息,需要自动根据json字符串中的字段来写入到hive不同的表对应的路径中。发送到Kafka中的数据原本最外层原本没有pkDay和project,只有data和name。因为担心data里面会空值,所以根同事商量,让他们在最外层添加了project和pkDay字段。pkDay字段用于表的自动分区,proejct和name合起来用于自动拼接hive表的名称为 ..._flume拦截器自定义开发 kafka

java同时输入不同类型数据,Java Spring中同时访问多种不同数据库-程序员宅基地

文章浏览阅读380次。原标题:Java Spring中同时访问多种不同数据库 多样的工作要求,可以使用不同的工作方法,只要能获得结果,就不会徒劳。开发企业应用时我们常常遇到要同时访问多种不同数据库的问题,有时是必须把数据归档到某种数据仓库中,有时是要把数据变更推送到第三方数据库中。使用Spring框架时,使用单一数据库是非常容易的,但如果要同时访问多个数据库的话事件就变得复杂多了。本文以在Spring框架下开发一个Sp..._根据输入的不同连接不同的数据库

随便推点

EFT试验复位案例分析_eft电路图-程序员宅基地

文章浏览阅读3.6k次,点赞9次,收藏25次。本案例描述了晶振屏蔽以及开关电源变压器屏蔽对系统稳定工作的影响, 硬件设计时应考虑。_eft电路图

MR21更改价格_mr21 对于物料 zba89121 存在一个当前或未来标准价格-程序员宅基地

文章浏览阅读1.1k次。对于物料价格的更改,可以采取不同的手段:首先,我们来介绍MR21的方式。 需要说明的是,如果要对某一产品进行价格修改,必须满足的前提条件是: ■ 1、必须对价格生效的物料期间与对应会计期间进行开启; ■ 2、该产品在该物料期间未发生物料移动。执行MR21,例如更改物料1180051689的价格为20000元,系统提示“对于物料1180051689 存在一个当前或未来标准价格”,这是因为已经对该..._mr21 对于物料 zba89121 存在一个当前或未来标准价格

联想启天m420刷bios_联想启天M420台式机怎么装win7系统(完美解决usb)-程序员宅基地

文章浏览阅读7.4k次,点赞3次,收藏13次。[文章导读]联想启天M420是一款商用台式电脑,预装的是win10系统,用户还是喜欢win7系统,该台式机采用的intel 8代i5 8500CPU,在安装安装win7时有很多问题,在安装win7时要在BIOS中“关闭安全启动”和“开启兼容模式”,并且安装过程中usb不能使用,要采用联想win7新机型安装,且默认采用的uefi+gpt模式,要改成legacy+mbr引导,那么联想启天M420台式电..._启天m420刷bios

冗余数据一致性,到底如何保证?-程序员宅基地

文章浏览阅读2.7k次,点赞2次,收藏9次。一,为什么要冗余数据互联网数据量很大的业务场景,往往数据库需要进行水平切分来降低单库数据量。水平切分会有一个patition key,通过patition key的查询能..._保证冗余性

java 打包插件-程序员宅基地

文章浏览阅读88次。是时候闭环Java应用了 原创 2016-08-16 张开涛 你曾经因为部署/上线而痛苦吗?你曾经因为要去运维那改配置而烦恼吗?在我接触过的一些部署/上线方式中,曾碰到过以下一些问题:1、程序代码和依赖都是人工上传到服务器,不是通过工具进行部署和发布;2、目录结构没有规范,jar启动时通过-classpath任意指定;3、fat jar,把程序代码、配置文件和依赖jar都打包到一个jar中,改配置..._那么需要把上面的defaultjavatyperesolver类打包到插件中

VS2015,Microsoft Visual Studio 2005,SourceInsight4.0使用经验,Visual AssistX番茄助手的安装与基本使用9_番茄助手颜色-程序员宅基地

文章浏览阅读909次。1.得下载一个番茄插件,按alt+g才可以有函数跳转功能。2.不安装番茄插件,按F12也可以有跳转功能。3.进公司的VS工程是D:\sync\build\win路径,.sln才是打开工程的方式,一个是VS2005打开的,一个是VS2013打开的。4.公司库里的线程接口,在CmThreadManager.h 里,这个里面是我们的线程库,可以直接拿来用。CreateUserTaskThre..._番茄助手颜色

推荐文章

热门文章

相关标签