音视频系列--c语言学习(结构体,指针,位运算,内存管理,异常指针)

C语言是学习音视频开发必须要掌握的,当然也没有必要学得多么深,只需要掌握常用的一些用法就可以了,这里记录下常用的语法。

一、结构体


1.1、结构体类型的定义

struct Person{
    char name[64];
    int age;
};

typedef struct {
    char name[64];
    int age;
}Person;

注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器还没有为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。

1.2、结构体变量的定义

struct Person{
    char name[64];
    int age;
}p1; //定义类型同时定义变量


struct{
    char name[64];
    int age;
}p2; //定义类型同时定义变量


struct Person p3; //通过类型直接定义

1.3、结构体变量的初始化

struct Person{
	char name[64];
	int age;
}p1 = {"john",10}; //定义类型同时初始化变量

struct{
	char name[64];
	int age;
}p2 = {"Obama",30}; //定义类型同时初始化变量

struct Person p3 = {"Edward",33}; //通过类型直接定义

1.4、结构体成员的使用

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

struct Person{
	char name[64];
	int age;
};
void test(){
	//在栈上分配空间
	struct Person p1;
	strcpy(p1.name, "John");
	p1.age = 30;
	//如果是普通变量,通过点运算符操作结构体成员
	printf("Name:%s Age:%d\n", p1.name, p1.age);

	//在堆上分配空间
	struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
	strcpy(p2->name, "Obama");
	p2->age = 33;
	//如果是指针变量,通过->操作结构体成员
	printf("Name:%s Age:%d\n", p2->name, p2->age);
}

注意在栈上和堆上定义结构体变量的区别

1.5、结构体赋值

相同的两个结构体变量可以相互赋值,把一个结构体变量的值拷贝给另一个结构体,这两个变量还是两个独立的变量。

struct Person{
	char name[64];
	int age;
};

void test(){
	//在栈上分配空间
	struct Person p1 = { "John" , 30};
	struct Person p2 = { "Obama", 33 };
	printf("Name:%s Age:%d\n", p1.name, p1.age);
	printf("Name:%s Age:%d\n", p2.name, p2.age);
	//将p2的值赋值给p1
	p1 = p2;
	printf("Name:%s Age:%d\n", p1.name, p1.age);
	printf("Name:%s Age:%d\n", p2.name, p2.age);
}

//一个老师有N个学生
typedef struct _TEACHER{
	char* name;
}Teacher;


void test(){
	
	Teacher t1;
	t1.name = malloc(64);
	strcpy(t1.name , "John");

	Teacher t2;
	t2 = t1;

	//对手动开辟的内存,需要手动拷贝
	t2.name = malloc(64);
	strcpy(t2.name, t1.name);

	if (t1.name != NULL){
		free(t1.name);
		t1.name = NULL;
	}
	if (t2.name != NULL){
		free(t2.name);
		t1.name = NULL;
	}
}

1.6、结构体数组

struct Person{
	char name[64];
	int age;
};

void test(){
	//在栈上分配空间
	struct Person p1[3] = {
		{ "John", 30 },
		{ "Obama", 33 },
		{ "Edward", 25}
	};

	struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
	for (int i = 0; i < 3;i ++){
		printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
	}
	printf("-----------------\n");
	for (int i = 0; i < 3; i++){
		printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
	}
	printf("-----------------\n");
	//在堆上分配结构体数组
	struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
	for (int i = 0; i < 3;i++){
		sprintf(p3[i].name, "Name_%d", i + 1);
		p3[i].age = 20 + i;
	}
	for (int i = 0; i < 3; i++){
		printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
	}
}

二、指针强化


指针是一种数据类型,占用内存空间,用来保存内存地址

2.1、指针变量

void test01(){
	
	int* p1 = 0x1234;  //1
	int*** p2 = 0x1111;

	printf("p1 size:%d\n",sizeof(p1));  //2
	printf("p2 size:%d\n",sizeof(p2));


	//指针是变量,指针本身也占内存空间,指针也可以被赋值
	int a = 10;
	p1 = &a;

	printf("p1 address:%p\n", &p1); //存放p1值元素的地址
	printf("p1 address:%p\n", p1);  //a元素的地址
	printf("a address:%p\n", &a);   //a元素的地址 

}

在这里有两点需要注意的是:
1.定义的指针是指向一个地址的,如果直接如注释1处赋值一个值,那么就是一个地址变量,地址是0x1234,变量是空的。
2.指针变量的size在window 64位是固定8个字节,window 32位是固定4个字节

2.2、空指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未执行任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。

不允许向NULL和非法地址拷贝内存

void test(){
	char *p = NULL;
	//给p指向的内存区域拷贝内容
	strcpy(p, "1111"); //err

	char *q = 0x1122;
	//给q指向的内存区域拷贝内容
	strcpy(q, "2222"); //err		
}

2.3、野指针

在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

什么情况下回导致野指针?

1、指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
2、指针释放后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
3、指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

void test(){
	int* p = 0x001; //未初始化
	printf("%p\n",p);
	*p = 100;
}

操作野指针是非常危险的操作,应该规避野指针的出现:

1、初始化时置 NULL
指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。
2、释放时置 NULL
当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

2.4、间接访问操作符

通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*

注意:对一个int*类型指针解引用会产生一个整型值,类似地,对一个float*指针解引用会产生了一个float类型的值。

在指针声明时, * 号表示所声明的变量为指针
在指针使用时, * 号表示操作指针所指向的内存空间
1) * 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
2) * 放在等号的左边赋值(给内存赋值,写内存)
3) * 放在等号的右边赋值 (从内存中取值,读内存)

//解引用
void test01(){

	//定义指针
	int* p = NULL;
	//指针指向谁,就把谁的地址赋给指针
	int a = 10;
	p = &a;
	*p = 20;//*在左边当左值,必须确保内存可写
	//*号放右面,从内存中读值
	int b = *p;
	//必须确保内存可写
	const char* str = "hello world!";
	*str = 'm';

	printf("a:%d\n", a);
	printf("*p:%d\n", *p);
	printf("b:%d\n", b);
}

2.5、指针的步长

指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;

//为什么*p1打印出来正确结果?
printf("%x\n", *p1);        //aabbccdd
//为什么*p2没有打印出来正确结果?
printf("%x\n", *p2);        //dd
//为什么p1指针+1加了4字节?
printf("p1  =%d\n", p1);    //p1  =6421980
printf("p1+1=%d\n", p1 + 1);//p1+1=6421984 
//为什么p2指针+1加了1字节?
printf("p2  =%d\n", p2);    //p2  =6421980
printf("p2+1=%d\n", p2 + 1);//p2+1=6421981

1.在上面的例子中定义指针类型和指向的类型需要一致,才能正确获取到值,否则获取的值会不正确。
2.定义的int类型指针类型步长为4,char类型指针类型步长为1.指针的步长为指向的数据类型

三、位运算


不论在c或者java中,位运算都是经常使用的,所以这里记录下使用

3.1、位逻辑运算符

3.1.1、按位取反~

一元运算符~将每个1变为0,将每个0变为1


~(10011010)
01100101

unsigned char a = 2;   //00000010
unsigned char b = ~a;  //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253
3.1.2、位与(AND) &

二进制运算符&通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是1时结果才为1。

   (10010011) 
 & (00111101) 
 = (00010001)

C也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:

val &= 0377
val = val & 0377

一个数 &1的结果就是取二进制的最末位。这可以用来判断一个整数的奇偶,二进制的最末位为0表示该数为偶数,最末位为1表示该数为奇数。

3.1.3、位或(OR) |

二进制运算符|通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为1,那么结果位就为1.

	(10010011)
  | (00111101)
  = (10111111)

C也有组合位或-赋值运算符: |=

val |= 0377
val = val | 0377

or运算通常用于二进制特定位上的无条件赋值,例如一个数or 1的结果就是把二进制最末位强行变成1。如果需要把二进制最末位变成0,对这个数or 1之后再减一就可以了。

3.1.4、位异或

二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是1(但不是都是1),那么结果是1.如果都是0或者都是1,则结果位0.

	(10010011)
  ^ (00111101)
  = (10101110)

C也有一个组合的位异或-赋值运算符: ^=

val ^= 0377
val = val ^ 0377
3.1.5、用法
3.1.5.1、打开位

已知:10011010:

1.将位2打开
flag | 00000100

(10011010)
|(00000100)
=(10011110)

2.将所有位打开
flag | ~flag

(10011010)
|(01100101)
=(11111111)
3.1.5.2、关闭位

flag & ~flag

(10011010)
&(01100101)
=(00000000)
3.1.5.3 、转置位

转置(toggling)一个位表示如果该位打开,则关闭该位;如果该位关闭,则打开。你可以使用位异或运算符来转置。其思想是如果b是一个位(1或0),那么如果b为1则b^1为0,如果b为0,则1^b为1。无论b的值是0还是1,0^b为b.

flag ^ 0xff

(10010011)
^(11111111)
=(01101100)
3.1.5.4、交换两个数不需要临时变量
  int a = 10;
  int b = 30;
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;

3.2、移位运算符

移位运算符将位向左或向右移动。

3.2.1、左移 <<

左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用0填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。

(10001010) << 2
(00101000)
1 << 1 = 2;
2 << 1 = 4;
4 << 1 = 8;
8 << 2 = 32

左移一位相当于原值*2.

3.2.2、右移 >>

右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。

//有符号值
(10001010) >> 2
(00100010)     //在某些系统上的结果值

(10001010) >> 2
(11100010)     //在另一些系统上的结果值

//无符号值
(10001010) >> 2
(00100010)    //所有系统上的结果值

3.2.3、用法:移位运算符

移位运算符能够提供快捷、高效(依赖于硬件)对2的幂的乘法和除法。

  1. number << n number乘以2的n次幂
  2. number >> n 如果number非负,则用number除以2的n次幂

找比cap大的2次幂最近值

#include <stdio.h>

 int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    return  n + 1;
}

四、指针详解


4.1、什么是指针

C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量。

img
这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。

4.2、为什么要使用指针

在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:

1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

4.3、指针表达式

 char ch = 'a';
 char *cp = &ch;

现在有了两个变量,一个ch(字符类型变量),一个cp(指向字符类型的指针变量),这个比较好理解。

接下来看下面常用操作

ch
&ch
cp
&cp
*cp
*cp+1
*(cp+1)
++cp
cp++
*++cp
*cp++
++*cp
(*cp)++
++*++cp
++*cp++

4.4、如何定义一个指针

  1. int p; //这是一个普通的整型变量
  2. int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
  3. int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
  4. int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比高,所以P 是一个数组,然后再与结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
  5. int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
  6. int **p; //首先从P 开始,先与结合,说是P 是一个指针,然后再与结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.
  7. int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
  8. int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
  9. int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

理解了这几个类型,其它的类型对我们来说类似,不过我们一般不会用太复杂的类型,那样会大大减小程序的可读性,指针还是需要谨慎使用,这上面的几种类型已经足够我们用了。

4.5、运算符优先级

1.::{作用域}
2.(){函数调用,类型构造:type(exp)},[]{下标},.{成员选择},->{成员选择}
3.++{后置},--{后置},typeid{类型id},explicit_cast{四种类型转换}
4.++{前置},--{前置},~{取反},!{逻辑非},-{一元负},+{一元正},\*{指针指向值},&{取地址},(){老式类型转换},sizeof{对象大小}
5.sizeof{类型或参数包的大小},new{分配内存},delete{释放内存},noexcept{能否抛出异常}
6.->\*{指向成员中的指针},.\*{指向成员中的指针}
7.*,/,%
8.+,-
9.<<,>>
10.<,>,<=,>=
11.==,!=
12.&

4.6、指针自身类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例子中各个指针的类型:

(1)int * ptr;//指针的类型是int *

(2)char * ptr;//指针的类型是char *

(3)int ** ptr;//指针的类型是int **

(4)int( *  ptr)[3];//指针的类型是int(*)[3]

(5)int *  ( * ptr)[4];//指针的类型是int*(*)[4]

4.7、指针所指向的类型(重要)

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:

(1)int * ptr; //指针所指向的类型是int

(2)char * ptr; //指针所指向的的类型是char

(3)int * * ptr; //指针所指向的的类型是int*

(4)int( * ptr )[3]; //指针所指向的的类型是int()[3]

(5)int * (  * ptr)[4]; //指针所指向的的类型是int *()[4]

在指针的算术运算中,指针所指向的类型有很大的作用,它决定了前面讲的步长。

指针的类型(即指针本身的类型)指针所指向的类型是两个概念。

每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)**

4.8、指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。例如:

char a[20] = {'a', 'b', 'c', 'd','e'};
int *ptr=(int *)a; //强制类型转换并不会改变a 的类型
ptr++;
printf("char %c\n", *a);
printf("char %c\n", *ptr);

在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),int 占4 个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:

int array[4]={0};
int *ptr=array;
//printf("ptr : 0x%x\n",  ptr );
for(int i=0;i<4;i++)
{
    (*ptr)++;
    ptr++;
}
for (int i = 0; i < 4; ++i) {
    printf("value: %d\n", *(array+i));
}
//printf("ptr : 0x%x\n",  ptr );
//printf("ptr : %d\n",  *(ptr-1) );

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1 个单元,所以每次循环都能访问数组的下一个单元。

char a[20]="You_are_a_girl";
int *ptr=(int *)a;
printf("* ptr addr %p\n",  ptr);
ptr+=1;
printf("* ptr addr %p\n",  ptr);
printf("* ptr=%c\n", *ptr);

在这个例子中,ptr 被加上了1,编译器是这样处理的:将指针ptr 的值加上1 乘sizeof(int), 由于地址的单位是字节,故现在的ptr 所指向的地址比起加4 后的ptr 所指向的地址来说,向高地址方向移动了4 个字节。

假设 加的不是1 而是 加的是 5 呢? ptr+=5;

​没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,ptr 是被减去5,那么处理过程大同小异,只不过ptr 的值是被减去5 乘sizeof(int),新的ptr 指向的地址将比原来的ptr 所指向的地址向低地址方向移动了20 个字节。

下面再举一个例子

char a[20]="You_are_a_girl";
char *p=a;
char **ptr=&p;

printf("ptr=0x%x\n",ptr);
printf("p=%c\n",*p);
printf("**ptr=%c \n",**ptr);
printf("beafor ptr= 0x%x\n", ptr);
ptr++;

printf("after ptr= 0x%x\n", ptr);
printf("ptr= %c\n",**ptr);

误区一、输出答案为Y 和o
误解:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char),所以输出如上结果,这个可能只是少部分人的结果.
误区二、输出答案为Y 和a
误解:ptr 指向的是一个char *类型,当执行ptr++;时,会使指针加一个sizeof(char *)(有可能会有人认为这个值为1,那就会得到误区一的答案,这个值应该是4,参考前面内容), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?答案是否定的.

正解: ptr 的类型是char ,指向的类型是一个char * 类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char),即&p+4;那(&p+4)指向哪呢,这个你去问上帝吧,或者他会告诉你在哪?所以最后的输出会是一个随机的值,或许是一个非法操作.

4.9、指针总结

一个指针ptr 加(减)一个整数n 后,

  1. 结果是一个新的指针ptr_new,ptr_new 的类型和ptr_old 的类型相同,
  2. ptr_new 所指向的类型和ptr_old所指向的类型也相同。
  3. ptr_new 的值将比ptr_old 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。

就是说,ptr_new 所指向的内存区将比ptr_old 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptr_old 所指向的类型)个字节。

指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义

对自身的指针可以进行加减1操作,一般用在数组方面。

五、内存分配


C语言的标准内存分配函数:malloccallocreallocfree等。

区别:

malloccalloc的区别为1个size与n个size大小内存的区别:

5.1、使用方式

malloc调用形式为(类型 ) malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。

calloc调用形式为(类型 ) calloc(n,size):在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。

realloc调用形式为(类型) realloc(*ptr,size):将ptr内存大小增大到size。

free的调用形式为(类型) free(void *ptr):释放ptr所指向的一块内存空间。

共同点:

  • 都为了分配存储空间,
  • 它们返回的是 void * 类型,也就是说如果我们要为int或者其他类型的数据分配空间必须显式强制转换

不同点:

  • malloc一个形参,因此如果是数组,必须由我们计算需要的字节总数作为形参传递
    malloc只分配空间不初始化,也就是依然保留着这段内存里的数据,
  • calloc 2个形参 ,因此如果是数组,需要传递个数和数据类型
    而calloc则进行了初始化,calloc分配的空间全部初始化为0,这样就避免了可能的一些数据错误。

六、内存管理机制


内存资源是非常有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何有效地管理内存资源。

6.1、变量概念:

  • 全局变量(外部变量):出现在代码块{}之外的变量就是全局变量。
  • 局部变量(自动变量):一般情况下,代码块{}内部定义的变量就是自动变量,也可使用auto显示定义。
  • 静态变量:是指内存位置在程序执行期间一直不改变的变量,用关键字static修饰。
    代码块内部的静态变量只能被这个代码块内部访问,代码块外部的静态变量只能被定义这个变量的文件访问。

6.2、extern关键字:

1、引用同一个文件中的变量;
2、引用另一个文件中的变量;
3、引用另一个文件中的函数。

注意:C语言中函数默认都是全局的,可以使用static关键字将函数声明为静态函数(只能被定义这个函数的文件访问的函数)。


这里写图片描述

6.3、程序执行流程:

这里写图片描述

代码区:

程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。

静态区

静态区存放程序中所有的全局变量和静态变量。

栈区

栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。

每个线程都有自己专属的栈;
栈的最大尺寸固定,超出则引起栈溢出;
变量离开作用域后栈上的内存会自动释放。

int main(int argc, char* argv[])
{
    char array_char[1024*1024*1024] = {0};
    array_char[0] = 'a';
    printf("%s", array_char);
    getchar();
}

栈溢出怎么办呢?就该堆出场了。

堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决内存溢出困难。一般比较复杂的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。

那堆内存如何使用?

malloc函数用来在堆中分配指定大小的内存,单位为字节(Byte),函数返回void *指针;free负责在堆中释放malloc分配的内存。

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

void print_array(char *p, char n)
{
    int i = 0;
    for (i = 0; i < n; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
}

int main(int argc, char* argv[])
{
    char *p = (char *)malloc(1024 * 1024 * 1024);//在堆中申请了内存
    memset(p, 'a', sizeof(int)* 10);//初始化内存
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        p[i] = i + 65;
    }
    print_array(p, 10);
    free(p);//释放申请的堆内存
    getchar();
}

这样就解决了刚才栈溢出问题。堆的容量有多大?理论上讲,它可以使用除了系统占用内存空间之外的所有空间。实际上比这要小些,比如我们平时会打开诸如QQ、浏览器之类的软件,但这在一般情况下足够用了。不能将一个栈变量的地址通过函数的返回值返回,如果我们需要返回一个函数内定义的变量的地址该怎么办?可以这样做:

int *getx()
{
    int *p = (int *)malloc(sizeof(int));//申请了一个堆空间
    return p;
}

int main(int argc, char* argv[])
{
    int *pp = getx();
    *pp = 10;
    free(pp);
    return 0;
}
//类似创建链表时,新增一个节点。

可以通过函数返回一个堆地址,但记得一定用通过free函数释放申请的堆内存空间。

分析:

这里写图片描述

main函数和UpdateCounter为代码的一部分,故存放在代码区

数组a默认为全局变量,故存放在静态区

main函数中的”char *b = NULL”定义了自动变量b(variable),故其存放在栈区

接着malloc向堆申请了部分内存空间,故这段空间在堆区

需要注意以下几点:

栈是从高地址向低地址方向增长;

在C语言中,函数参数的入栈顺序是从右到左,因此UpdateCounter函数的3个参数入>栈顺序是a1、c、b;

C语言中形参和实参之间是值传递,UpdateCounter函数里的参数a[1]、c、b与静态区>的a[1]、c、b不是同一个;

6.4、内存管理的目的

学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:

  • 如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
  • 如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
  • 如果需要动态创建数组,则用堆。

创建动态数组:

//动态创建数组
int main()
{
    int i;
    scanf("%d", &i);
    int *array = (int *)malloc(sizeof(int) * i);
    //...//这里对动态创建的数组做其他操作
    free(array);
    return 0;
}12345678910

操作系统在管理内存时,最小单位不是字节,而是内存页(32位操作系统的内存页一般是4K)。比如,初次申请1K内存,操作系统会分配1个内存页,也就是4K内存。4K是一个折中的选择,因为:内存页越大,内存浪费越多,但操作系统内存调度效率高,不用频繁分配和释放内存;内存页越小,内存浪费越少,但操作系统内存调度效率低,需要频繁分配和释放内存。

七、异常指针


悬空指针是这样一种指针:指针正常初始化,曾指向过一个正常的对象,但是对象销毁了,该指针未置空,就成了悬空指针。

野指针 是这样一种指针:未初始化的指针,其指针内容为一个垃圾数。 (一般我们定义一个指针时会初始化为NULL或者直接指向所要指向的变量地址,但是如果我们没有指向NULL或者变量地址就对指针进行使用,则指针指向的内存地址是随机的)。存在野指针是一个严重的错误。

int main() {
    int *p; // 指针未初始化,此时 p 为野指针
    int *pi = nullptr;
 
    {
        int i = 6;
        pi = &i; // 此时 pi 指向一个正常的地址
        *pi = 8; // ok
    }  
 
    *pi = 6; // 由于 pi 指向的变量 i 已经销毁,此时 pi 即成了悬空指针
 
    return 0;
}

7.1、什么是空指针

​如果 p 是一个指针变量,则 p = 0; p = 0L; p = '\0'; p = 3 - 3; p = 0 * 17; 中的任何一种赋值操作之后(对于 C 来说还可以是 p = (void*)0;), p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。(比如这里的(void*)0就是一个空指针)

7.1、什么是NULL指针

NULL 是一个标准规定的宏定义,用来表示空指针常量。因此,除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。与上一种情况相似,只不过是空指针的一种例子

7.2、为什么通过空指针读写的时候就会出现异常?

NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值