-
系列文章目录
- c语言指针
- 项目开发常用字符串应用模型
文章目录
作用域
作用域就是它能在哪去使用。生命周期就是从哪开始从哪结束,例如:夏虫朝菌、浮游朝生暮死、昙花一现······
C语言变量的作用域分为:
- 代码块作用域(代码块就是 { } 之间的一段代码)
- 函数作用域
- 文件作用域
局部变量
使用auto修饰,局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块 {} 内部定义的变量都是自动变量,它有如下特点:
- 在函数内部定义的变量,只在函数范围内有效;
- 在复合语句中定义,只在复合语句中有效;
- 如果没有赋初值,内容为随机
-
- 局部变量未初始化,初值为随机值:C规范对该初值并没有做规定,具体实现由编译器决定。
-
- 全局变量未初始化,默认设置为初值。
-
- 局部未初始化变量,编译通过与否与具体编译环境相关。
-
- 作用域:在函数内部,从变量定义到函数结束。
- 生命周期:从变量定义到函数结束。
- 随着函数调用的结束或复合语句的结束局部变量的生命周期也结束
局部变量存在栈区。
复合语句(compound statement)简称为语句块,它使用大括号 { } 把许多语句和声明组合到一起,形成单条语句。
{ [声明和语句的列表] }
加不加auto都指的是局部变量,一般都不加auto进行修饰,跟声明extern一样不加。
#include <stdio.h>
void Add(int a,int b)//形参也是局部变量
{
int c = 0;//局部变量
}
int main(void)
{
/*
在函数内部定义的变量 局部变量
作用域:在函数内部,从变量定义到函数结束
生命周期:从变量定义到函数结束
*/
auto int a = 6;//定义变量 局部变量
for (int i = 0; i < 10; i++) {} //局部变量 i 作用域只限于for函数中
//printf("%d\n", i);//err
printf("%d\n", a);//6
return 0;
}
全局变量
- 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,需用 extern 声明;
- int j; 或者 extern int j;
不声明找不到
- 作用域:整个项目中所有文件,如果在其他文件中使用,需要声明,但不能重复定义,不同文件的全局变量不能重复。
- 全局变量的生命周期和程序运行周期一样;
- 生命周期:从程序创建到程序销毁。
- 全局变量存的不是栈区,
存在数据区。
- 不同文件的全局变量不可重名
- 局部变量可以和全局变量同名,不冲突,局部变量在栈区,是两个不同的变量,在操作的时候,数据是采取就近原则。
test1.c
#include <stdio.h>
//全局变量 在函数外部定义的变量
//作用域:整个项目中所有文件 如果在其他文件中使用 需要声明
//生命周期:从程序创建到程序销毁
int j = 1;
void Func02()
{
j = 666;
printf("%d\n", j);
}
int main()
{
printf("%d\n", j);//1
Func02();//666
Func03();//666
return 0;
}
test2.c
#include <stdio.h>
extern int j;//声明
void Func03()
{
printf("%d\n", j);
}
如果不声明会报错,只有声明了编译才能通过。
#include <stdio.h>
int j = 1;
int main()
{
printf("%d\n", j);//1
int j = 60;
printf("%d\n", j);//60
//匿名内部函数
{ //这里叫代码体(程序体)
//int j = 52;//第一种 ------------------------------
j = 52;//第二种 ------------------------------
printf("%d\n", j);//52
} //程序结束之后这个函数就销毁了
printf("%d\n", j);//第一种:60 ; 第二种:52
return 0;
}
还可以打印地址看看printf("%p\n", &j);
extern全局变量声明
extern int a; 声明一个变量,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义。
静态变量
存储的位置:数据区
静态(static)局部变量
- static局部变量的作用域也是在定义的函数内有效,只能在函数内部使用
- static局部变量的生命周期和程序运行周期一样,从程序创建到程序销毁,同时staitc局部变量的值
只初始化一次,但可以赋值多次。
- 局部变量要优于函数,静态局部变量在
数据区
进行存储,程序在执行起来的时候,static这句话就已经走完一遍了 - static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
#include<stdio.h>
void Func01()
{
//int y = 1;//局部变量
static int y = 1;//静态局部变量
y++;
printf("%d\n", y);
}
int main()
{
//静态局部变量
//static int y = 1;
//printf("%d\n", y);
for (int i = 0; i < 10; i++)
{
Func01();
}
return 0;
}
静态(static)全局变量
作用域:可以在本文件中使用,但是不可以在其他文件中使用
。
生命周期:从程序创建到程序销毁。
#include <stdio.h>
//静态全局变量
static int c = 1;
void Func()
{
c = 666;
printf("%d\n", c);
}
int main()
{
for (int i = 0; i < 10; i++)
{
c++;
printf("%d\n", c);
}
Func();
}
不可以在其他文件中使用静态全局变量,不然会报错
未初始化
局部变量未初始化在Visual Studio中是不允许使用的,因为虽然内存开辟空间了,但是打印的值都是乱码(任意值)
。在其他的编译环境中是允许使用的,Visual Studio做一个安全限制,你没有初始化它一定是错的,它会给你进行报错。
未初始化的全局变量:0
未初始化的静态全局变量:0
未初始化的静态局部变量:0
全局函数和静态函数
全局函数
在C语言中函数默认都是全局的(全局函数),extern可省略。项目中的所有文件都可以去调用它。
声明函数
extern 类型标识符 函数名( 形参列表 );
或者
类型标识符 函数名( 形参列表 );
其实不声明程序能找到
,但是如果你声明了你就可以右键跳转,找起来比较方便,声明是有一定意义的,声明一般是放在头文件中( .h文件),建议声明
。
C++有多态可以方法重载;C语言不支持一个函数名对应多个参数的样式,所以全局函数名称是作用域中唯一的。
作用域: 在整个项目中所有文件中使用。
生命周期:从程序创建到程序销毁
静态函数(内部函数)
在C语言中函数默认都是全局的,使用关键字static
可以将函数声明为静
态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使
用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
对于不同文件中的staitc函数名字可以相同。
static 类型标识符 函数名( 形参列表 );
作用域:当前文件中
静态函数可以和全局函数重名,当前文件中优先调用静态函数,就近原则。当静态函数和全局函数重名并且在同一文件中时,报错。
生命周期:从程序创建到程序销毁
#include <stdio.h>
static void FuncStatic()
{
printf("静态函数\n");
}
int main()
{
FuncStatic();
return 0;
}
注意事项
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。(就近原则)
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。
内存布局
内存分区
内存不止四区,内存四区只是对于我们应用程序来说的,内存四区模型图:
全局常量,安全的常量 ,存储区域为数据区常量区。局部常量是不安全的。
- 代码区:程序执行的二进制码(程序指令)。存放CPU执行的机器指令。另外,代码区还规划了局部变量的相关信息。
- 特点
- 共享
通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。 - 只读
代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。
- 共享
- 特点
- 数据区:加载的信息都是跟程序同生共死的
- 全局初始化数据区/静态数据区(
data段 或者 data segment
)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。 - 未初始化数据区(又叫
bss区
)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)。 - 常量区
- 全局初始化数据区/静态数据区(
- 栈区
(stack)
:系统为每一个程序分配一个临时的空间- 一般存局部变量、函数信息、函数参数、数组、指针、结构体。
- 栈区大小为1M,在windows中可以扩展到10M、在Linux中可以扩展到16M
- 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
- 存储变量的时候是
从高地址开始存
,向下增长(只有栈区是这样的)
,数组是例外首元素地址是从低地址开始。 - 函数入栈是从后向前入栈,例如函数 void func(int a,int b){} ,先存b再存a。
- 入栈是从高地址到低地址,出栈是从低地址到高地址,
先进后出、后进先出(沙桶原理)
- 堆区
(heap)
- 公共的区域
- 大小:理论是没有任何限制的,跟内存有关,除了上面三个区域剩下的全都是堆区,内存越大堆区越大,没有栈那样先进后出的顺序。用于动态内存分配。
- 存储大数据、图片、音频文件
- 需要手动开辟内存空间,malloc、colloc、realloc,是一块连续的空间,这个连续的空间会获取一个指针,这里面没有变量名,只能通过指针来操作。
- 需要手动释放,free
- 一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
- 从低地址开始存,向上增长
存储类型总结
#include <stdio.h>
//全局常量 安全的常量 存储区域为数据区常量区
const int c1 = 123;
//未初始化的全局变量
int x1;
//初始化全局变量
int x2 = 52;
//未初始化的静态全局变量
static int s1;
//初始化静态全局变量
static int s2 = 16;
int main()
{
const int c2 = 123;//局部常量 不安全的
int y = 10;
//未初始化的局部变量
static int j1;
//初始化的局部变量
static int j2 = 10;
char* p = "hello xy";//字符串常量
int arr[] = { 1,2,3,4 };//数组
int** pp = arr;//指针
printf("未初始化的全局变量: % p\n", &x1);
printf("未初始化的静态全局变量: % p\n", &x2);
printf("未初始化的静态全局变量: % p\n", &s1);
printf("初始化静态全局变量: % p\n", &s2);
printf("局部变量: % p\n", &y);
printf("字符串常量: % p\n", p);
printf("数组: % p\n", &arr);
printf("指针变量: % p\n", pp);
printf("指针地址: % p\n", &pp);
return 0;
}
可以看到红色的存储区域都是数据区,里面又有一些区别,初始化的、未初始化、字符串常量。
绿色的是栈区,栈区一般存局部变量、数组信息、指针、结构体。
堆区内存分配和释放
栈区超出1M会报错,我们定义一个数组看看效果
#include <stdio.h>
int main()
{
int arr[1000000] = { 0 };//超出1M(默认),报错
/*
字节B 1000000*4=4000000
KB 4,000,000/1024=3,906.25
MB 3,906.25/1024=3.81
*/
//int arr[1000000/4] = { 0 };//没有超出
return 0;
}
1.第一种方式(设置)
默认是1M,如果想要扩充,通过程序编译之前,右键项目选择属性,windows中最大可改为10M,默认单位为byte,修改为10M,1010241024=10485760
#include <stdio.h>
int main()
{
//栈区大小
//int arr[1000000] = { 0 };//超出1M,报错
/*
字节B 1000000*4=4000000
KB 4,000,000/1024=3,906.25
MB 3,906.25/1024=3.81
*/
int arr[1000000/4] = { 0 };//没有超出
return 0;
}
2. 开辟堆空间存储数据
malloc
#include <stdlib.h>
void* malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的
连续区域
,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
- size:需要分配内存大小(单位: 字节)
返回值:
- 成功:分配空间的起始地址,
- 失败:NULL
free
#include <stdlib.h>
void free(void* ptr);
功能: 释放ptr指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址,对同一内存空间多次释放会出错。
参数:
ptr:需要择放空间的首地址,被释放区应是由malloc函数所分的区域。返回值: 无
注意:开辟和释放必须是同一个指针
就算没有free(),main()结束后也是会自动释放malloc()的内存的,free()的用处在于实时回收内存。如果你的程序很简单,那么你不写free()也没关系。
#include <stdio.h>
int main()
{
//开辟堆空间存储数据
int* p = (int*)malloc(sizeof(int));
printf("%p\n",p);//01685FE0
printf("%d\n",*p);//-842150451
//使用堆空间
*p = 123;
printf("%d\n", *p);//123
//释放堆空间
free(p);
printf("释放完之后:%p\n", p);//01685FE0
printf("释放完之后:%d\n", *p);//-572662307
*p = 456;
printf("%d\n", *p);//456
return 0;
}
释放完还能操作这个堆空间地址吗?答案是可以,释放完p之后这个p就变成野指针了,p指向未知空间了,操作野指针对应的空间是可能报错的,可能也不报错的,我们尽量避免野指针的出现,可以在每次使用完p之后p=NULL;
把p置为空。
注意:开辟堆空间的时候,要有连续的空间,如果没有就失败了
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
//int* p = (int*)malloc(sizeof(int) * 1000000);//3M 打印的地址:00EE4040
//int* p = (int*)malloc(sizeof(int) * 1000000 * 100);//300M 打印的地址:00EE4040
//int* p = (int*)malloc(sizeof(int) * 1000000 * 1000);//3000M==2.92GB 打印的地址:00000000
int* p = (int*)malloc(sizeof(int) * 1000000 * 1000 / 3);//1000M==1GB 打印的地址:01288040
printf("%p\n",p);//
free(p);
return 0;
}
一般情况下, 加载到内存中的数据最多控制在1个G左右。
堆空间存储数据
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
//开辟10个内存大小
int* p = malloc(sizeof(int) * 10);//开辟了40字节
//一般不写
//if (!p) {//p == NULL
// printf("程序异常\n");
// return -1;
//}
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
for (int i = 0; i < 10; i++)
{
//printf("%d\n", p[i]);
printf("%d\n", *(p+i));
}
free(p);
return 0;
}
还可以放随机数
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//随机数导入stdlib、time.h文件
#define MAX 10
int main()
{
srand((size_t)time(NULL));
int* p = (int*)malloc(sizeof(int) * MAX);
for (int i = 0; i < MAX ; i++)
{
p[i] = rand() % 100;
printf("%d\n",p[i]);
}
free(p);
return 0;
}
内存操作函数
1) memset
#include <string.h>
void * memset(void *s, int c, size_t n);
功能: 将的内存区域的前n个
字节
以参数填入,栈区堆区都可以
参数:
- s:需要剩作内存s的首地址
- c: 填的疗符.c虽然参数为int,但必须是unsigned char,范围为0~255
- n:指定需要设置的大小
返回值: s的首地址
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);//40
//memset()重置内存空间的值
//memset(p,0,40);//打印全是0
memset(p,1,40);//打印全是16843009
for (int i = 0; i < 10; i++)
{
printf("%d\n",p[i]);
}
free(p);
return 0;
}
一般都是用 memset(p,0,40); 重置成0。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char ch[10];//栈区
memset(ch,'y',10);
for (int i = 0; i < 10; i++)
{
printf("%c\n",ch[i]);
}
return 0;
}
2) memcpy
#include <string.h>
void * memcpy(void *dest, const void * src , size_t n);
功能: 拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
- dest:目的内存首地址
- src: 源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错,内存里面一边往里读一边往里写,可能会导致内存被占用掉
- n:需要拷贝的字节数
返回值: dest的首地址
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int arr[10] = { 0,9,1,2,3,4,5,6,7,8 };
int* p = (int*)malloc(sizeof(int)*10);
memcpy(p, arr, sizeof(int) * 10);
for (int i = 0; i < 10; i++)
{
printf("%d\n",p[i]);
}
free(p);
return 0;
}
字符串拷贝strcpy 和 内存拷贝memcpy 的区别
- strcpy 字符串拷贝遇到\0停止,只会将第一个\0之前的内容拷贝过来。
- 内存拷贝memcpy 不管你是不是\0都拷贝,拷贝的内容和字节有关,和内容无关。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char ch[] = "hello\0 xy";
char s[100];
//strcpy(s,ch);//字符串拷贝遇到\0就停止
memcpy(s,ch,9);
for (int i = 0; i < 9; i++)
{
printf("%c",s[i]);
}
return 0;
}
当拷贝源和拷贝目标发生重叠
#include <stdio.h>
#include <string.h>
int main()
{
int arr[] = {0,1,2,3,4,5,6,7,8,9};
memcpy(&arr[5], &arr[3], 12);//0 1 2 3 4 3 4 5 8 9
for (int i = 0; i < 9; i++)
{
printf("%d ",arr[i]);
}
return 0;
}
0,1,2,3,4,5,6,7,8,9 ==> 0 1 2 3 4 3 4 5 8 9
3) memmove
memmove() 功能用法和 memcpy() 一样,区别在于:dest和src所指的内存空间重叠时,memmove() 仍然能处理,不过执行效率比memcpy() 低些。
如果源和目标重叠,它会自己在内存中开辟一块空间,先把你 源 挪到这去,然后再挪到目标文件,这样就不会报错了。
不重叠的时候跟memcpy一样
#include <stdio.h>
#include <string.h>
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
//memcpy(&arr[5], &arr[3], 12);
memmove(&arr[5], &arr[3], 12);//0 1 2 3 4 3 4 5 8
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
4) memcmp
#include <string.h>
void * memcmp(const void *s1, const void *s2, size_t n);
功能: 比较s1和s2所指向内存区域的前n个字节,比较的是内存中的值,不限于字符串、整型······
参数:
- s1:内存首地址s1
- s2:内存首地址s2
- n:需比较的前n个字节
返回值:
- 相等:=0
- 大于:>0
- 小于:<0
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int arr2[] = { 0,1,2,3,4,5 };
int val= memcmp(arr1,arr2,20);
printf("%d\n", val);//0
return 0;
}
比较字符串:strcmp(s1,s2)只能比较 \0 之前的内容;memcmp可以比较\0之后,它比较的跟字节有关。
#include <stdio.h>
#include <string.h>
int main()
{
char s1[] = "hello\0 xy";
char s2[] = "hello\0 xy";
int val= memcmp(s1,s2,10);
printf("%d\n", val);//0
return 0;
}
内存常见问题
1)越界
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//数组下标越界
//char ch[8] = "hello xy";
//堆空间数组下标越界
char* p = (char*)malloc(sizeof(char) * 8);
strcpy(p,"hello xy");
printf("%s\n",p);
/* 我开辟了8空间,但是我用了9个空间,
释放是按照8释放还是9释放?按8释放没释放掉,按9释放释放多了
释放多余的空间会出现问题 */
//free(p);//一用就报错
return 0;
}
2)开辟释放0个字节的空间
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(0);//野指针
printf("%p\n", p);
*p = 666;
printf("%d\n", *p);
//free(p);//一用就报错
return 0;
}
3)开辟内存空间能存2.5个整型
一般情况建议用int* p = (int*)malloc(sizeof(int)*10);
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(10);
p[0] = 0;
p[1] = 11;
//p[2] = 22;//一用就报错
printf("%p\n", p);
printf("%d\n", *p);
printf("%d\n", *(p+1));
free(p);
return 0;
}
4)释放之后再释放
第一次释放是正确指针,第二次释放是野指针,释放野指针是不对的,操作野指针空间可能报错,释放野指针一定报错。堆空间不允许多次释放
我们用完给它置为空指针,就不会报错啦,堆空间不允许多次释放,空指针允许多次释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
free(p);
p = NULL;//空指针
free(p);
free(p);
free(p);
free(p);
free(p);
return 0;
}
5)开辟和释放不是同一个指针
通过指针操作对应的堆空间的时候,尽量不要修改,如果需要修改最好做一个备份。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
printf("%p\n",p);
for (int i = 0; i < 10; i++)
{
//指针叠加 不断改变指针方向,释放会出错
*p++ = i;
}
printf("%p\n", p);
free(p);//这时候的p地址不是一开始的地址了
return 0;
}
正确:备份
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
//printf("%p\n",p);
int* temp = p;
for (int i = 0; i < 10; i++)
{
//指针叠加 不断改变指针方向,释放会出错
//*p++ = i;
*temp++ = i;
}
//printf("%p\n", p);
free(p);//这时候的p地址不是一开始的地址了
return 0;
}
6)指针 值传递
因为这两个p都是一级指针(同一级别的),虽然两个都是指针,但是指针传递的情况下,这种方式也是叫值传递。这种方式并没有堆空间地址传递过来。
#include <stdio.h>
#include <stdlib.h>
void Func01(int * p)
{
p = (int*)malloc(sizeof(int) * 10);
}
int main()
{
int* p = NULL;
Func01(p);
for (int i = 0; i < 10; i++)
p[i] = i;
free(p);
return 0;
}
我们想要地址传递应该这样写:
解决办法1
用更高级的指针接收它
#include <stdio.h>
#include <stdlib.h>
void Func01(int** p) //
{
*p = (int*)malloc(sizeof(int) * 10);//
}
int main()
{
int* p = NULL;
Func01(&p);//
for (int i = 0; i < 10; i++)
p[i] = i;
free(p);
return 0;
}
打印地址看看
解决办法2
返回值去接收
#include <stdio.h>
#include <stdlib.h>
int* Func03()
{
int* p = malloc(sizeof(int) * 10);
return p;
}
int main()
{
int* p = NULL;
p = Func03(&p);
for (int i = 0; i < 10; i++)
p[i] = i;
for (int i = 0; i < 10; i++)
printf("%d ",p[i]);
free(p);
return 0;
}
打印0 1 2 3 4 5 6 7 8 9。因为堆空间在函数Func03结束之后不会被释放,所以p操作堆空间不会报错。
二级指针对应的堆空间
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
//int p[5][3];
//开辟二级指针对应的堆空间
int** p = (int**)malloc(sizeof(int*) * 5);
//开辟5行3列的二维数组
for (int i = 0; i < 5; i++)
{
//开辟一级指针对应的堆空间
p[i] = (int*)malloc(sizeof(int) * 3);
}
//存值
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 3; j++)
{
scanf("%d", &p[i][j]);
}
}
//取值
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", p[i][j]);
}
puts("");
}
//先释放里面的再释放外面的
for (int i = 0; i < 5; i++)
{
free(p[i]);
}
free(p);
return 0;
}
注意:不是连续的堆空间
输入
1 2 3
4 5 6
7 8 9
10 11 12
13 14 15