动态内存申请
数组的长度是预先定义好的,在整个程序中固定不变,但是在实际编程中,所需的内存取决于实际输入的数据,无法预先确定。为了解决这些问题,C语言提供了内存管理函数,可以按需动态的分配内存空间,也可以把不再需要的空间回收再次利用。
所谓动态内存申请其实就是在堆区开辟空间
静态和动态分配
静态分配
- 在程序编译或运行过程中,按事先规定的大小分配内存空间的方式
- 必须事先知道所需空间大小
- 分配在栈区或全局变量区,一般以数组的形式
动态分配
- 在程序运行过程中,根据需要大小自由分配所需空间
- 按需分配
- 分配在堆区,一般使用特定的函数进行分配
动态分配函数
#include <stdlib.h>
void *malloc(unsigned int size);
在内存的动态存储区(栈区)中分配一块长度为size字节的连续内存
,用来存放类型说明符指定的类型。
函数原型返回void*指针,使用时必须做相应的强制类型转换,分配
的内存空间内容不确定,一般使用memset初始化。分配成功则返回
分配空间的`起始地址`;NULL(分配失败)
注意:
1. 在调用malloc之后,一定要判断一下,是否申请内存成功
2. 如果多次malloc申请的内存,第一次和第二次申请的内存不一定是连续的
3. 使用malloc开辟空间需要保存开辟好的空间的首地址,但是由于不确定空间用于存储什么,所以返回值类型为void*,所以在调用函数时根据接收者的类型对其进行强制类型转换
#include <stdio.h>
//qt中会自动识别然后加上该句,但是其他平台可能就报错了
#include <stdlib.h>
char *func(){
//一般都强制转换,虽然不强转也可以但是会报警告
char *str= (char *)malloc(100*sizeof(char));
str[0]='h';
str[1]='e';
str[2]='l';
str[3]='l';
str[4]='0';
str[5]='\0';
return str;
}
int main(int argc, char *argv[])
{
char *p;
p=func();
printf("p=%s\n",p); //p=hell0
return 0;
}
free函数(释放内存的函数)
#include <stdlib.h>
void free(void *ptr)
free函数释放ptr指向的内存(释放堆区的空间)
注意:ptr指向的内存必须时malloc calloc relloc动态申请的内存,而且是全部释放,不能局部释放
char *p=(char *)malloc(100);
free(p);
1. free后,因为没有给P赋值,所以p还是指向原先动态申请的内存
但是内存已经不能使用,p变成了野指针.虽然地址还是有效的,但是已经不确定内部存放的是什么,所以建议不要在使用。一般都是free完毕之后给p=NULL
2. 一块动态申请的内存只能free一次,不能多次
calloc函数
#include <stdlib.h>
void* calloc(size_t nmemeb,size_t size)
size_t 实际是无符号整型,它是在头文件中,用typedef定义出来的
函数功能:在内存堆中,申请nmemeb块,每块的大小为size个字节的连续区域,实际大小为nmemeb*size
函数返回值:
成功:首地址
失败: NULL
例如:
char *p=(char*)calloc(3,100);
在堆中申请了3块,每块大小为100个字节,即300个字节的连续的区域
区别:
1. 参数个数不同
2. malloc申请的内存,内存中存放的内容是随机的,不确定的,
而calloc申请的内存中的内容为0,也就是说calloc会把申请内存
的内容清空然后置为0
realloc函数(重新申请内存)
调用malloc和calloc函数单次申请的内存是连续的,两次申请的不见得连续。
有些时候需要申请连续的内存,例如先用malloc和calloc申请的内存之后还想挨着申请内存,就需要realloc函数了。
void (*p)realloc(void *s,unsigned int newsize);
函数功能:
在原先s指向内存基础上重新申请内存,新的内存大小为new_size
个字节,如果原先内存后面有足够大的空间,就追加,否则
realloc会在堆区找一个newsize字节大小的内存申请,将原先内
存中的内容拷贝过来,然后释放原先的内存,最后返回新的地址。
如果newsize比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节
返回值:新申请的内存地址
char *p;
p=(char *)malloc(100);
//想在1oo个字节后面追加50个字节
p=(char *)realloc(p,150);//p指向的内存的新的大小为150个字节
注意:malloc calloc relloc动态申请的内存,只有在free或程序结束时候才释放
内存泄漏
申请的内存,首地址丢了,无法使用了,也无法释放,则这块内存就是泄露了
p=(char*)malloc(100);
p="hello";//p保存了字符串常量的首地址
//p指向别的地方了,则内存泄漏了
void func(){
char *p;
p=(char*)malloc(100);
//然后再也不使用了,也算内存泄漏
}
//则该函数每次被调用则泄漏100字节空间
字符串处理函数
字符串长度
#include<string.h>
函数定义:size_t strlen(const char *s);
函数功能:获取字符指针s指向的字符串中实际字符的个数,不包含'\0',遇到第一个'\0'则结束
#include <stdio.h>
#include<string.h>
int main(int argc, char *argv[])
{
char s[100]="hello";
printf("%d\n",strlen(s));//5
printf("%d\n",sizeof(s));//100
char *s1="hello";
printf("%d\n",strlen(s1)); //5
printf("%d\n",sizeof(s1));//4 指针都是4个字节
return 0;
}
拷贝字符串
#include<string.h>
函数定义:char *strcpy(char *dest,const char *src);
函数说明:
拷贝src指向的字符串到dest指针指向的内存中,'\0'也会拷贝
函数返回值:目的内存的地址
注意:使用此函数,必须保证dest指向的内存空间足够大,否则会出现内存污染
char *strncpy(char *dest,const char *src,size_t n);
函数说明:
将src指向的字符串前n个字节,拷贝到dest指向的内存中
函数返回值:目的内存的地址
注意:
1. strncpy不拷贝'\0'
2. 如果n大于src指向的字符串中的字符个数,则在dest后面填充n-strlen(src)个'\0'
int main(int argc, char *argv[])
{
char s[100]="hello";
char s1[100]="world";
strcpy(s,s1);
printf("%s\n",s);//world
return 0;
}
追加函数
#include<string.h>
函数定义:char *strcat(char *dest,const char *src);
strcat函数追加src字符串到dest指向的字符串后面,追加的时候会追加'\0'
注意:保证dest指向的内存空间足够大
#include <stdio.h>
int main(int argc, char *argv[])
{
char s[100]="hello";
char s1[100]="world";
strcat(s,s1);
printf("%s\n",s);//helloworld
return 0;
}
函数定义:char *strncat(char *dest,const char *src.size_t n);
追加src指向的字符串的前n个字符,到dest指向的字符串的后面。
注意:如果n大于src的字符个数,则只将src字符串追加到dest指向的字符串的后面,追加的时候会追加'\0'
比较函数
#include<string.h>
函数定义:char *strcmp(const char *s1,const char *s2);
函数说明:
比较s1和s2指向的字符串的大小
比较方法:逐个字符去比较ascii码,一旦比较出大小立即返回而不会继续比较
返回值:
如果s1指向的字符串大于s2指向的字符串,返回1
如果s1指向的字符串小于s2指向的字符串,返回-1
相等返回0
int main(int argc, char *argv[])
{
char s[100]="hello";
char s1[100]="world";
//其实此处比较到w就结束了,因为是一个个字符比较的
int ret=strcmp(s,s1);
printf("%d\n",ret);//-1
return 0;
}
int strncmp(const char *s1,const char *s2,size_t n);
比较前N个字符是否一样,返回值同上
查找函数
#include<string.h>
函数定义:char *strchr(const char *s,int c);
函数说明:
在字符指针s指向的字符串中,找ascii码为c的字符
注意:是首次匹配,如果s指向的字符串有多个c的字符,则找到的是第一个字符
返回值:找到了返回对应字符的地址,否则返回NULL
int main(int argc, char *argv[])
{
char s[]="hello";
char *ret=strchr(s,'h');
printf("%p\n",ret);//0060FE96
printf("%d\n",ret-s);//0
return 0;
}
函数定义:char *strrchr(const char *s,int c);
函数说明:末次匹配
在s指向的字符串中,找最后一次出现的ascii为c的字符
返回值:末次匹配的字符的地址,否则NULL
匹配函数
#include<string.h>
函数定义:char *strstr(const char *haystack,const char * needle);
函数说明:
在haystack指向的字符串中查找needle指向的字符串,也是首次匹配
返回值:找到了的字符串的首地址,否则NULL
int main(int argc, char *argv[])
{
char s[]="hello";
//重要:查找的是第一个\0之前的字符串
char *ret=strstr(s,"ll");
printf("%p\n",ret);//0060FE98
printf("%d\n",ret-s);//2
return 0;
}
字符串转换数值
#include<string.h>
int atoi(const char *nptr);
将nptr指向的字符串转换成整数,返回
int num;
num=atoi("123");//123
long atoi(const char *nptr);
double atoi(const char *nptr);
字符串切割函数
#include<string.h>
char *strtok(char *str,const char *delim);
字符串切割,按照delim指向的字符串中的字符,切割str指向的字符串。
其实就是在str指向的字符串中发现delim字符串中的字符,就将其变成'\0'
调用一次strtok只切割一次,切割一次之后,再去切割的时候strtok的第一个参数传NULL,意思是接着上次切割的位置继续切割
注意:如果str字符串中出现了连续的几个delim中的字符,则只将第一个字符变成'\0'
如果没了则则返回NULL,所以可以用NULL做while判断条件
int main(int argc, char *argv[])
{
char s[]="111:222:333:555";
char *ret;
ret=strtok(s,":");
printf("%s\n",ret);//111
ret=strtok(NULL,":");
printf("%s\n",ret);//22
ret=strtok(NULL,":");
printf("%s\n",ret);//333
ret=strtok(NULL,":");
printf("%s\n",ret);//555
return 0;
}
格式化字符串操作函数
int sprintf(char *buf,const char *format,...);
\\输出到bug指定的内存区域
char buf[20];
sprintf(buf,"%d:%d:%d",2013,10,1);
printf("buf=%s\n",buf);
int sscanf(const char *buf,const char *format,...)
\\从bug指定的内存区域中读取信息
sscanf("2013:10:1","%d:%d:%d",&a,&b,&c);
printf("%d %d %d\n",a,b,c);
const(重要)
1. 修饰普通变量,代表只读的意思
//全局变量只能使用,不能修改
const int a=100;
void test(){
int *p=&a;
*p=88;//这种修改也是错误的,编译直接报错或者运行时错误
}
//const修饰局部变量,不能通过变量名修改,但是可以通过指针修改值
void test(){
const int a=100;
// a=200;//报错
int *p=&a;
*p=88;
}
2. const 修饰指针
- const char *str
意思是str指向的内存的内容不能通过str来修改
用来保护str指向的内存的内容
char *strcpy(char *dest,const char *str);
如果const修饰指针变量的类型,无法通过指针变量修改地址里面保存的值
void test(){
int c=100;
const int *p=&c;
// *p=777;//错误
}
如果const修饰指针变量,无法修改指针变量保存的地址
void test(){
int c=100;
int const *p=&c;
*p=777;//正确
int d=888;
//p=&d;//报错
}
如果即const修饰指针变量也修饰指针变量的类型,则都不行
结构体、共用体、枚举
构造类型:不是基本类型的数据结构也不是指针,它是若干个相同或不同类型的数据构成的集合
描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算,有时我们需要将不同类型的数据组合成一个有机的整体
结构体
- 数组用于保存多个相同类型的数据
- 结构体用于保存多个不同类型的数据
结构体是一个构造类型的数据结构;是一种或多种基本类型或构造类型的数据的集合
//一般结构体类型都会定义在全局,也就是main函数的外面,所以在定义结构体类型的同时定义变量,这些变量一般都是全局变量
struct stu{
int num;
char name[20];
char sex;
}lucy,bob;
//定义完类型之后定义的结构体变量内存分配要看定义的位置
struct stu xiaoming;
无名结构体
struct{
int num;
char name[20];
char sex;
}lucy,bob;
因为没有定义结构体类型名,所以不能在以后再定义相关类型的结构体变量了,只能再定义类型的同时定义结构体变量
给结构体类型取别名
通常将一个结构体类型重新起个类型名,用新的类型名替代原先的类型
typedef struct stu{
int num;
char name[20];
char sex;
}STU;
//此处结构体名字可有可无,因为下面已经重新起别名了STU
//STU就相当于 struct stu等
结构体变量的定义和初始化
#include <stdio.h>
struct stu{
int num;
char name[20];
char sex;
}lisi={
1002,"lisi",'m'
};
struct stu xiaoming;
int main(int argc, char *argv[])
{
struct stu wangwu;
struct stu zs={1001,"sss",'m'};
return 0;
}
结构体变量的使用
#include <stdio.h>
struct stu{
int num;
char name[20];
char sex;
}lisi={
1002,"lisi",'m'
};
struct stu xiaoming;
int main(int argc, char *argv[])
{
struct stu wangwu;
struct stu zs={1001,"sss",'m'};
// zs.name="aaa"; 这样直接修改不行,相当于字符串常量地址赋值给数组名,数组一旦定义好数据名就是地址常量了不可修改
strcpy(zs.name,"aaa");
printf("%s\n",zs.name);//aaa
return 0;
}
#include <stdio.h>
#include<string.h>
typedef struct{
int year;
int age;
}BD;
struct stu{
int num;
char name[20];
char sex;
//结构体嵌套
BD hehe;
};
int main(int argc, char *argv[])
{
struct stu xiaoming;
strcpy(xiaoming.name,"小明");
//嵌套结构体赋值
xiaoming.hehe.age=20;
return 0;
}
相同类型的结构体变量可以互相赋值
#include <stdio.h>
#include<string.h>
struct stu{
int num;
char name[20];
char sex;
};
int main(int argc, char *argv[])
{
struct stu xiaoming;
struct stu lisi;
lisi=xiaoming;
return 0;
}
结构体数组
就是一个数组,由若干个相同类型的结构体变量构成的集合
#include <stdio.h>
#include<string.h>
struct stu{
int num;
char name[20];
char sex;
};
int main(int argc, char *argv[])
{
struct stu person[3]={
{101,"lucy",78},
{102,"lucy",79},
{103,"lucy",80}
};
return 0;
}
结构体指针
即结构体的地址,结构体变量存放内存中,也有起始地址;
定义一个变量来存放这个地址,那这个地址变量就是结构体指针变量。
结构体指针变量对成员的引用
- (*结构体指针变量名).成员
- 结构体指针变量名->成员
#include <stdio.h>
#include<string.h>
struct stu{
int num;
char name[20];
char sex;
};
int main(int argc, char *argv[])
{
struct stu person;
struct stu *p;
p=&person;
// (*p).num=100;等效下面
p->num=101;
strcpy(p->name,"asdas");
printf("%s\n",person.name);//asdas
return 0;
}
但是既然有结构体直接使用,为什么还需要这个结构体指针呢?
//结论:堆区开辟内存时候需要
//定义结构体指针变量
struct stu *s;
//注意:需要强转,因为返回的是void类型的指针void *
s=(struct stu *)malloc(sizeof(struct stu));
结构体内存分配问题
结构体变量大小是,它所有成员之和,因为结构体变量是所有成员的集合。但是实际分配时候是有规则的。
- 规则1:以多少个字节为单位开辟内存
给结构体变量分配内存的时候,会去结构体变量中找基本类型的成员,哪个基本类型的成员占字节数多,就以它大小为单位开辟内存
在gcc中出现double类型的,例外
1. 成员中只有char型数据,以1字节为单位开辟内存
2. 成员中出现了short类型数据,没有更大字节数的基本类型数据,就以2字节为单位开辟内存
3. 出现了 int float没有更大字节的基本类型数据的时候以4字节为单位开辟内存
4. 出现了double类型的数据
情况1:
在vc里,以8字节为单位开辟内存
情况2
在gcc里,以4字节为单位开辟内存
无论是那种环境,double型变量,占8字节
注意:如果在结构体中出现了数组,数组可以看成多个变量的集
合。如果出现指针的话,没有占字节数更大的类型的,以4字节为单位开辟内存.
在内存中存储结构体成员的时候,按定义的结构体成员的顺序存储
举例
struct stu{
char sex;
int age;
} lucy;
lucy 的大小4 的倍数
- 规则2:字节对齐
1. char 1 字节对齐,即存放char 型的变量,内存单元的编号是1的倍数即可
2. short int 2 字节对齐,即存放short int 型的变量,起始内存单位的编号是2的倍数即可
3. int 4字节对齐,即存放int 型的变量,起始内存单元的编号是4的倍数即可
4. long 在32位平台下,4字节对齐,即存放long int 型的变量,起始内存单元的编号是4的倍数即可
5. float 4 字节对齐,即存放 float 型的变量, 起始内存单元的编号是4的倍数即可
double
- vc环境下
8字节对齐,即存放double型变量的起始地址,必须是8的倍数,double变量占8个字节
- gcc环境下
4字节对齐,即存放double型变量的起始地址,必须是4的倍数,double变量占8个字节
注意:当结构体成员中出现数组的时候,可以看出多个变量
注意:开辟内存的时候,从上向下依次按成员在结构体中的位置顺序开辟空间



为什么要有字节对齐
用空间来换时间,提高cpu读取数据的效率
位段(位域)
一般位段在底层或者硬件使用,而应用层次开发一般只到字节
- 在大多数的计算机系统中, 一个字节是由八个更小的, 称作为位的单位组成的。位是比字节更小的单位。位只有两个值, 1 或 0 。因此, 存储在计算机存储器中的一个字节可以看成由八个二进制数字形成的串。
- 例如, 一个存放值 36 的字节是八个二进制数字的串: 可以表示成 00100100。 存入值24 的字节可以表示成 00010100。
- 注意: 不能对位段成员取地址,因为内存以字节为单位
struct packed_data {
unsigned int a:2;
unsigned int b:6;
unsigned int c:4;
unsigned int d:4;
unsigned int i;
}data;
位段注意
对于位段成员的引用如下
data.a = 2
赋值时,不要超出位段定义的范围
如段成员 a定义为2位,最大值为3,即(11) 2
所以 data.a = 5, 就会取5的低两位进行赋值 101->01 就会出错
位段成员的类型必须指定为整型或字符型
一个位段必须存放在一个存储单元中,不能跨两个单元,第一个单元空间不能容纳下一个位
段,则该空间不用, 而从下一个单元起存放该位段
位段的存储单元
char 型位段 存储单元是1个字节
short int 型的位段存储单元是2个字节
int 的位段,存储单元是4字节
long int 的位段,存储单元是4个字节
struct stu{
char a:7;
char b:7;
char c:2;
}temp; //占3个字节,b不能跨存储单元存储
//结果为:3 证明位段不能跨其存储单元存储
//不能取 temp.b的地址,因为b可能不够1字节,不能取地址
位段的长度不能大于存储单元的长度
char 型 位段不能大于 8位
short int 型位段 不能大于 16位
int 型的位段,位段不能大于32位
long int的位段,位段不能大于32位
struct stu{
char a:9;
char b:7;
char c:2;
} temp;
int main(){
printf("%d\n",sizeof(temp));
} //编译出错, 位段a不能大于其存储单元的大小
如一个段要从另一个存储单元开始,可以定义
unsigned char a:1;
unsigned char b:2;
unsigned char :0;
unsigned char c:3; (另一个单元)
由于用了长度为0 的位段,其作用是使下一个位段从下一个存储单元开始存放
将a、b存储在一个存储单元中,c另存在下一个单元
struct m_type{
unsigned char a:1;
unsigned char b:2;
unsigned char :0;
unsigned char c:3;
};
可以定义无意义位段 如:
unsigned a:1;
unsigned d:2;
unsigned b:3;
struct data{
char a:1;
char b:1;
char c:1;
char d:1;
char e:1;
char f:1;
char g:1;
char h:1;
}temp;
int main(){
char p0;
//p0 = 0x01; //0000 0001
temp.a = 1;
//p0 = temp; //错的,类型不匹配
//p0 = (char)temo;//错的,编译器不允许将结构体变量。强制转成基本类型的字符型
p0 = *((char *)(&temp));
}
共用体
共用体和结构体类型,也是一种构造类型的数据结构,关键字是union
在进行某些算法的时候,需要使几种不同类型的变量存在同一段内存单元中,几个变量所使用空间相互重叠,这种几个不同的变量共同占用一段内存的结构,在C语言中,被称为"共用体"类型结构,共用体所有成员占有同一段地址空间
共用体的大小是其占内存长度最大的成员的大小
union stu{
int num;
char name[20];
char sex;
};
- 同一内存可以用来存放几种不同类型的成员,但每一个瞬时只有一种起作用
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
- 共用体变量的地址和它的各成员的地址都是同一个地址
- 共用体变量的初始化,union data a={123};初始化为第一个成员
枚举
将变量的值一一列举出来,变量的值只限于列举出来的值的范围
枚举类型也是构造类型,enum
- 枚举值是常量,不能再程序中用赋值语句对它再赋值
- 枚举元素本身由系统定义了一个表示序号的数值,默认0开始,依次递增
- 可以改变枚举值的默认值
enum week{
mon=3,tue,wed=6,thu
};
int main(int argc, char *argv[])
{
enum week day=thu;
printf("%d\n",day);//7
return 0;
}
网友评论