01. 数据类型
数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。
数据类型基本概念:
- 类型是对数据的抽象;
- 类型相同的数据具有相同的表示形式、存储格式以及相关操作;
- 程序中所有的数据都必定属于某种数据类型;
- 数据类型可以理解为创建变量的模具: 固定大小内存的别名;
typedef关键字
typedef的作用:
- 给数据类型起别名
- 用来区分数据类型
- 提高移植型
#include <stdio.h> //标准i input输入 o output输出
#include <string.h> //对字符串处理函数 strcat strstr strcmp strcpy
#include <stdlib.h> //malloc free
//typedef 1.可以起别名
struct Person
{
char name[64];
int age;
}
typedef struct Person myPerson; //myPerson 就是别名
//mian函数,程序入口
int main()
{
myPerson p = {"李白",18};
//2.区分数据类型
char* p1,p2; //p1是char*类型,p2是char类型。但是很容易误解成他们都是char*类型
char *p3,*p4; //p3,p4都是char*类型了
typedef char* PCHAR;
PCHAR p5,p6; //p5,p6都是char*类型了
return 0;
}
void数据类型
void字面意思是”无类型”,不能用void创建变量,因为无法给无类型变量分配内存空间
void* 无类型指针,无类型指针可以指向任何类型的数据。
void的用途:
- 对函数返回值的限定;
- 对函数参数的限定;
- void* 万能指针的使用
#include <stdio.h>
//限定函数的返回值
void func01()
{
return; //不加void,return后面能写数据,加上void后,后面不能写数据
}
//限定函数参数
void func02(void) //不加void,能传实参。加上void后,不能传实参
{
return;
}
int main()
{
//func02(10); //err
return 0;
}
注意:void* 万能指针。 多级指针、任意类型指针在32位系统上都是4位
#include <stdio.h>
int main()
{
int* pInt = NULL;
char* pChar = NULL;
PChar = (char*)pInt; //需要强制转换
void* p = NULL;
PChar = p; //万能指针,不通过强制转换就能换成其他类型指针
return 0;
}
sizeof用法
sizeof本质:它不是一个函数,而是操作符
sizeof的返回值是,unsigned int无符号整型
sizeof的用途:数据类型大小计算、统计数组长度
printf("%d\n",sizeof(int)); //如果后面是数据类型,则sizeof后面必须更()小括号
double d = 3.14;
printf("%d\n",sizeof d); //如果后面的变量,则sizeof后面可以不加()小括号
#include <stdio.h>
//注意:当数组名做函数参数时,会退化成指针,指针指向的是数组的第一个元素。所以sizeof(arr)为4
void test01(int arr[])
{
printf("%d\n",sizeof(arr)); //结果是4
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8};
test01(arr);
retrun 0;
}
02. 作用域和声明周期
局部变量:也是auto变量(auto可省略),定义在函数内部的变量
作用域:从定义开始,到包裹该变量的代码块结束
生命周期:从变量定义开始,到函数调用完成。——当前函数
全局变量:定义在函数外部的变量
作用域:从定义位置开始,默认到文件内部。其他文件如果想使用,可以通过extern关键字 声明全局变量 将作用域导出
生命周期:程序启动开始,到程序终止结束。——程序执行期间
static全局变量:在全局变量定义之前添加 static关键字
作用域:限制在本文件内部,不允许通过 extern关键字声明导出到其他文件
生命周期:程序启动开始,到程序终止结束。——程序执行期间
static局部变量:在局局变量定义之前添加 static关键字
特性:静态局部变量只定义一次,在全局内存中。通常用来做计数器
作用域:从定义开始,到包裹该变量的代码块结束
生命周期:程序启动开始,到程序终止结束。——程序执行期间
全局函数:类型 函数名(形参) + 函数体
作用域:整个程序
生命周期:程序启动开始,到程序终止结束。——程序执行期间
static函数:static 类型 函数名(形参) + 函数体
作用域:static函数只能在本文件内部使用。
生命周期:程序启动开始,到程序终止结束。——程序执行期间
#include <stdio.h>
int variate02; //全局变量,不赋初值,默认为0
static int svar02; //静态全局变量
void fun01() //全局函数
{
return;
}
static void fun02() //静态函数
{
return;
}
int main()
{
auto int variate01 = 10; //局部变量
static int svar01; //静态局部变量,不赋初值,默认为0
return 0;
}
03. 内存布局
由于Windows操作系统不开放源码,所以不清楚Windows平台下的内存怎么分布
Linux系统开发源码。所以可以看到内部实际存储
内存4区模型:
- 代码段:.text段。程序源代码(二进制形式)
- 数据段:
- 只读数据段 .rodata段,存放 常量
- 初始化数据段 .data段,存放 初始化为非0的全局变量和静态变量
- 未初始化数据段 .bss段,存放 未初始化的全局变量和静态变量,程序加载执行前,会将该段整体赋值为0
- stack:栈。空间小,系统自动管理、自动分配、自动释放。 特性是FILO(先进后出)。 Windows下默认是1M,可提升上限至10M。Linux下默认是8M,可提升上限至16M
- heap:堆。空间大,用户自己管理、分配、释放。特性是FIFO(先进先出)。约1.3G+
内存4区图如下(Linux版)
04. 内存分区
程序运行之前
编写C程序,会进行4步的操作:
- 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
- 编译:检查语法,将预处理后文件编译生成汇编文件
- 汇编:将汇编文件生成目标文件(二进制文件)
- 链接:将目标文件链接为可执行程序
当我们编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、**数据区(data)和未初始化数据区(bss)**3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
- 代码区(text段):存放 CPU 执行的机器指令(二进制),代码区是可共享的,也只读的
- 全局初始化数据区/静态数据区(data段):存放被初始化的全局变量和静态变量,还有常量数据(如:字符串常量)
- 未初始化数据区(bss段):存放未初始化的全局变量和静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
程序运行之后
运行可执行程序后,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
- 栈区(stack):栈是FILO先进后出的内存结构,容量较小,存放函数的参数值、返回值、局部变量等,由编译器自动分配和释放。因此,局部变量的生存周期为申请到释放该段栈空间。
- 堆区(heap):堆是FIFO先进先出的内存结构,容量很大,一般由程序员分配(malloc)和释放(free),若程序员不释放,程序结束时由操作系统回收。
05. 堆区管理函数
malloc() 和 free()
#include <stdlib.h>
void *malloc(size_t size);
功能:在堆区内存中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。
参数:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL
free()函数:释放空间
#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间。
参数:需要释放空间的首地址。
返回值:无
malloc()、free()的使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
//开辟堆空间
int *p = (int*)malloc(sizeof(int)*10);
if(p == NULL)
{
printf("malloc error\n");
return -1;
}
//释放堆空间
free(p);
p = NULL; //一般free完后,将该指针置为NULL
return 0;
}
使用heap空间注意事项:
- free后,空间不会失效。通常free后,地址置为NULL
- free地址必须是 malloc申请地址,否则free时会出错。(如:不要p++)
- 如果malloc之后的地址需要变化,那么可以使用临时变量temp保存原地址,然后free(temp);
calloc()
#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
功能:在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存置0。
参数:
参1:所需内存单元数量
参2:每个内存单元的大小(单位:字节)
返回值:
成功,分配空间的起始地址
失败,NULL
int *pp = calloc(10,sizeof(int)); //开辟10个int类型的堆区空间,并且置0
realloc()
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
功能:重新分配用malloc()或calloc()在堆中分配内存空间的大小。
参数:
参1:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
参2:为重新分配内存的大小, 单位:字节
返回值:
成功,新分配的堆内存地址
失败,NULL
int *pp = calloc(10,sizeof(int)); //开辟10个int类型的堆区空间,并且置0
pp = realloc(pp,sizeof(int)*10); //扩展堆区空间大小
注意:
- malloc()和calloc()的区别是,malloc()不会置0,calloc()会置0
- realloc()申请空间机制:
- 如果原有空间的后续空间足够大,则直接在后续申请空间
- 如果原因空间的后续空间不足,则会重新找一块内存,并将原有空间内的数据拷贝到新空间下
06. 存储类型操作函数
memset()
分配的内存空间内容不确定,一般使用memset初始化。
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
参1:需要操作内存s的首地址
参2:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
参3:指定需要设置的大小
返回值:s的首地址
memset()函数用法:
int a[10];
memset(a, 0, sizeof(a));
memset(a, 97, sizeof(a));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%c\n", a[i]);
}
memcpy()
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
参1:目的内存首地址
参2:源内存首地址,注意:dest和src所指的内存空间不可重叠
参3:需要拷贝的字节数
返回值:dest的首地址
memcpy()函数用法:
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10];
memcpy(b, a, sizeof(a));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d, ", b[i]);
}
memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。
memcmp()
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
参1:内存首地址1
参2:内存首地址2
参3:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0
memcmp()函数用法:
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int flag = memcmp(a, b, sizeof(a));
printf("flag = %d\n", flag);
07. 就近原则
#include <stdio.h>
int m = 100;
int main()
{
int m = 10;
printf("m = %d\n",m); //10
//不建议这样写
return 0;
}
不建议自己这样写。只需要知道有这么个原理,别人这么写代码时看的懂,就够了。
of(a));
printf(“flag = %d\n”, flag);
# 05. 就近原则
```c
#include <stdio.h>
int m = 100;
int main()
{
int m = 10;
printf("m = %d\n",m); //10
//不建议这样写
return 0;
}
不建议自己这样写。只需要知道有这么个原理,别人这么写代码时看的懂,就够了。
08. static和extern的区别
static静态变量特点:在运行前分配内存,程序运行结束 生命周期结束,所以在本文件都可以使用静态变量
extern:可以提高变量的作用域
//文件1
int a = 100; //在C语言下,全局变量前都隐式加了关键字extern
//文件2
#include <stdio.h>
int main()
{
extern int a; //告诉编译器,下面的a是外部链接属性,在其他文件中
printf("%d\n",a);
return 0;
}
09. 全局/静态区
int v1 = 10; //全局变量,存放在静态区
const int v2 = 20; //全局常量,存放在常量区,初始化后不可修改
static int v3 = 30; //静态全局变量,存放在静态区
void test()
{
const int v2 = 20; //const局部变量,存放在栈区,通过指针可以修改
char str[] = "hello"; //字符串常量
str[0] = 'a'; //有些编译器可以修改,有些编译器不能修改
//没有一个标准,所以尽量不要修改字符串常量的内容
}
10. 宏函数
#include <stdio.h>
//定义宏函数
#define MYADD(x,y) ((x)+(y))
int main()
{
int a = MYADD(10,20);
printf("%d\n",a);
return 0;
}
注意事项:
- 宏函数最好加小括号修饰,保证运算的完整性
- 通常将频繁、短小的函数,写成宏函数
- 宏函数会比普通函数在一定程度上效率高,省去了函数入栈、出栈的时间
- 宏函数后面 没有分号;
优点:以空间换时间
函数调用流程:
- 局部变量、函数形参、函数返回地址… 都要入栈和出栈
调用惯例:主调函数和被调函数必须有一致约定,才能正确的调用函数,这个约定就是调用惯例
- 调用惯例包含:出栈方、参数传递顺序、函数名称修饰
- C/C++默认调用惯例:cdecl——从右到左,主调函数惯例出栈
11. 栈的生长方向和内存存放方向
#include <stdio.h>
int main()
{
//栈的生长方向
int a = 10; //栈底,高地址
int b = 20;
int c = 30;
int d = 40; //栈顶,低地址
//内存存放方向
int i = 0x11223344;
char* p = &a;
printf("%x\n",*p); //44 低位地址数据
printf("%x\n",*(p+1)); //33
printf("%x\n",*(p+2)); //22
printf("%x\n",*(p+3)); //11
return 0;
}
高位数据存放高地址,低位数据存放低地址,这种存储方式 称为小端存储方式