c语言提高学习笔记——02-c提高01day
在学习c语言提高总结了笔记,并分享出来。有问题请及时联系博主:Alliswell_WP,转载请注明出处。
02-c提高01day
目录:
1、数据类型概念
2、typedef
3、void数据类型
4、sizeof操作符
5、数据类型总结与扩展
6、变量
7、程序的内存分区模型
8、总结
1、数据类型概念
什么是数据类型?为什么需要数据类型?
数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。
我们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有自己的类型,那么程序中使用数据类型也是来源于生活。
当我们给狗分配内存的时候,也就相当于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,我们可以给他们各自建造一个别墅,但是会造成内存的浪费,不能很好的利用内存空间。
我们在想,如果给鸟分配内存,只需要鸟窝大小的空间就够了,如果给狗分配内存,那么也只需要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,造成内存的浪费。
当我们定义一个变量,a=10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存可以放得下10?
所以说,数据类型非常重要,它可以告诉编译器分配多少内存可以放得下我们的数据。
狗窝里面是狗,鸟窝里面是鸟,如果没有数据类型,你怎么知道冰箱里放得是一头大象!
数据类型基本概念:
>类型是对数据的抽象;
>类型相同的数据具有相同的表示形式、存储格式以及相关操作;
>程序中所有的数据都必定属于某种数据类型;
>数据类型可以理解为创建变量的模具:固定大小内存的别名;
2、typedef
(1)为变量设置别名
1 typedef unsigned int u32; 2 typedef struct _PERSON{ 3 char name[64]; 4 int age; 5 }Person; 6 7 void test(){ 8 u32 val;//相当于unsigned int val; 9 Person person;//相当于struct PERSON person; 10 }
(2)同时定义多个指针类型,不会出错
1 typedef char* PCHAR 2 3 PCHAR p1, p2;
(3)定义一个类型,换平台后只用更改typedef后的变量
1 typedef long long mytype_t; 2 3 //有利于程序移植性
3、void数据类型
(1)void不能直接定义变量,因为编译器不知道分批额多少内存给变量
void a;//报错!
注意:当定义一个变量,编译器必须要知道分配多少内存(struct Person p一直递归,不知道分配多少内存)
1 #define _CRT_SECURE_NO_WARNINGS 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 6 struct Person 7 { 8 char name[64]; 9 int age; 10 struct Person p; 11 }; 12 13 void test01() 14 { 15 struct Person p; 16 } 17 18 int main() 19 { 20 test01();
system("pause");
return EXIT_SUCCESS; 21 }
(2)void使用情况:对函数返回的限定;对函数参数的限定
(3)void* 无类型指针,所有类型指针的祖宗(void *可以指向任何类型的数据)
void* p = NULL;
任何类型的指针都可以不经过强制转换,转换成void* 类型
int* pInt = NULL;
char* pChar = (char *)pInt; //需要强制转换
void* pVoid = pInt;//不需要强制转换
(4)void* 主要用于数据结构的封装
4、sizeof操作符
sizeof是c语言中的一个操作符,类似于++、--等等。sizeof 能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。
基本语法:
sizeof(变量);
sizeof 变量;
sizeof(类型);
sizeof 注意点:
>sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;
>sizeof返回的数据结果类型是unsigned int;
>要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小;
1 //1.sizeof基本用法 2 void test01(){ 3 4 int a=10; 5 6 printf("1en:%d\n",sizeof(a)); 7 printf("len:%d\n",sizeof(int)); 8 printf("len:%d\n",sizeof a); 9 } 10 11 //2.sizeof结果类型 12 void test02(){ 13 14 unsigned int a=10; 15 16 if(a-11<0){ //无符号的数a与有符号的数大多数编译器运算结果仍然是无符号的! 17 printf("结果小于0\n"); 18 } 19 else{ 20 printf("结果大于0\n"); 21 } 22 int b=5; 23 24 if(sizeof(b)-10<0){ 25 printf("结果小于0\n"); 26 } 27 else{ 28 printf("结果大于0\n"); 29 } 30 31 //3.sizeof 碰到数组 32 void TestArray(int arr[]){ 33 printf("TestArray arr size:%d\n",sizeof(arr)); 34 } 35 void test03{ 36 int arr[]={10,20,30,40,50}; 37 printf("array size:%d\n",sizeof(arr));//20 38 //数组名在某些情况下等价于指针 39 int* pArr=arr; 40 printf("arr[2]:%d\n",pArr[2]); 41 printf("array size:%d\n",sizeof(pArr)); 42 //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小 43 TestArray(arr);//4 44 }
C语言结构体所占用的字节数如何计算?
参考——https://zhidao.baidu.com/question/183477298.html
1 //格式一 2 struct tagPhone 3 { 4 char A; 5 int B; 6 short C; 7 }Phone; 8 //格式二 9 struct tagPhone 10 { 11 char A; 12 short C; 13 int B; 14 }Phone2; 15 //格式三 16 struct tagPhone3 17 { 18 char A; 19 char B[2]; 20 char c[4j; 21 }Phone3;
扩展:
我们都知道,char类型占用个字节,int型占用个字节,short类型占用个字节,long占用8个,double占用bai个;
那么我们可能会犯一个错误就是直接1+4+2=7,该结构体占用7个字节。这是错的。
以下我们简单分析下:
计算结构体大小时需要考虑其内存布局,结构体在内存中存放是按单元存放的,每个单元多大取决于结构体中最大基本类型的大小。
对格式一:
以int型占用个来作为倍数,因为A占用一个字节后,B放不下,所以开辟新的单元,然后开辟新的单元放C,所以格式一占用的字节数为:3*4=12;
同理对于格式二,
A后面还有三个字节,足够C存放,所以C根着A后面存放,然后开辟新单元存放B数据。所以格式二占用的内存字节为2*4=8
对于格式三:
上面结构计算大小,sizeof(Phone3) = 1+2 +4 = 7, 其大小为结构体中个字段大小之和,这也是最节省空间的一种写法。
总结:
第一种写法,空间浪费严重,sizeof 计算大小与预期不一致,但是保持了每个字段的数据类型。这也是最常见的漫不经心的写法,一般人很容易这样写;
第三种写法,最节省空间的写法,也是使用 sizeof 求大小与预期一样的写法,但是全部使用字节类型,丢失了字段本生的数据类型,不方便使用;
第二种写法,介于第一种和第三种写法之间,其空间上比较紧凑,同时又保持了结构体中字段的数据类型。
只要了解是这些写法的差异性,可以视情况选用。
5、数据类型总结与扩展
(1)数据类型本质是固定内存大小的别名;是个模具,c语言规定:通过数据类型定义变量。
(2)数据类型大小计算(sizeof)
(3)可以给已存在的数据类型起别名typedef
(4)数据类型封装概念(void 万能类型)
6、变量
int i= 10; //把10放到i所代表的内存中
变量就是一块内存(本质!)
既能读又能写的内存对象,称为变量;若一旦初始化后不能修改的对象则称为常量。
变量定义形式:类型标识符,标识符,…,标识符
变量名的本质
>变量名的本质:一段连续内存空间的别名;
>程序通过变量来申请和命名内存空间int a=0;
>通过变量名访问内存空间;
>不是向变量名读写数据,而是向变量所代表的内存空间中读写数据;
修改变量的两种方式:
1 void test(){ 2 int a=10; 3 4 //1.直接修改 5 a=20; 6 printf("直接修改,a:%d\n",a); 7 8 //2.间接修改 9 int*p=&a; 10 *p=30; 11 printf("间接修改,a:%d\n",a); 12 }
字节偏移
1 #define _CRT_SECURE_NO_WARNINGS 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 6 struct Person 7 { 8 char a; 9 int b; 10 char c; 11 int d; 12 } 13 14 void test02() 15 { 16 struct Person p = {'a', 100, 'b', 200}; 17 printf("p.d:%d\n",p.d); 18 19 p.d = 1000;//修改d 20 21 } 22 int main() 23 { 24 test02(); 25 printf() 26 system("pause"); 27 return EXIT_SUCCESS; 28 }
用字节偏移修改d
1 #define _CRT_SECURE_NO_WARNINGS 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 6 struct Person 7 { 8 char a; 9 int b; 10 char c; 11 int d; 12 } 13 14 void test02() 15 { 16 struct Person p = {'a', 100, 'b', 200}; 17 printf("p.d:%d\n",p.d); 18 19 p.d = 1000;//修改d 20 //(char *)&p +12结构体所占大小按最大的(int)计算,所以p.d的起始地址为:先计算起始地址&p,然后转成(char *)1个字节,然后加上12个字节 21 //printf("(char *)&p +12:%d\n",(char *)&p +12);//测试p.d的起始地址 22 //printf("&p.d:%d\n",&p.d);//测试p.d的起始地址 23 printf("(int *)((char *)&p +12):%d\n",(int *)((char *)&p +12));//找到d 24 } 25 int main() 26 { 27 test02(); 28 printf() 29 system("pause"); 30 return EXIT_SUCCESS; 31 }
7、程序的内存分区模型
流程说明
1、操作系统把物理硬盘代码load到内存
2、操作系统把c代码分成四个区
3、操作系统找到main函数入口执行
各区元素分析
全局区和全局静态区的区别?
int a;实际是extern int a;在当前项目所有文件中都可见。
static int a;是全局静态变量,在当前文件中都可见。
常量区:字符串常量如“hello world”和const修饰的全局变量
常量区特点:数据一旦初始化,不能修改!只读的内存!
(1)内存分区
1)运行之前
我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译。
1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:将目标文件链接为可执行程序
当我们编译完成生成可执行文件之后,我们通过在linux下 size命令可以查看一个可执行二进制文件基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
·代码区
存放CPU执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
·全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
·未初始化数据区(又叫bss区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)。
总体来讲说,程序源代码被编译之后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据域段和.bss段属于程序数据。
那为什么把程序的指令和程序数据分开呢?
■程序被load到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改;
■当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer7.0运行起来之后,它需要占用112844KB的内存,它的私有部分数据有大概15944KB,也就是说有96900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。
2)运行之后
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序 load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
此处说的分配内存,只是分配地址,而不是真正分配内存。真正分配内存在程序运行时。
·代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
·未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
·全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
·栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
程序自动分配释放!栈的大小固定!
·堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
堆的大小不固定,主要看机器的有多大!
(2)栈区
由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。
1 #define _CRT_SECURE_NO_WARNINGS 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 6 //1.栈区的内存自动申请自动释放,不需要程序手动管理 7 int* myFunc() 8 { 9 int a = 10; 10 return &a;//不要返回局部变量的地址 11 } 12 13 void test01() 14 { 15 //我们并不关心a的值是多少,因为局部变量a的内存已经被回收 16 int* p = myFunc(); 17 printf("*p = %d\n", *p); 18 } 19 20 char *getString() 21 { 22 char str[] = "hello world"; 23 return str; 24 } 25 26 void test02() 27 { 28 char *s = NULL; 29 s = getString(); 30 printf("s = %s\n", s);//返回报错!!! 31 } 32 33 int main(){ 34 35 //test01(); 36 test02(); 37 38 system("pause"); 39 return EXIT_SUCCESS; 40 }
分析过程
1 #char* func(){ 2 char p[]="hello world!";//在栈区存储乱码 3 printf("%s\n",p); 4 return p; 5 } 6 void test(){ 7 char*p=NULL; 8 p=func(); 9 printf("%s\n",p); 10 }
(3)堆区
由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
1 char* func(){ 2 char* str=malloc(100); 3 strcpy(str,"hello world!"); 4 printf("%s\n",str); 5 return str; 6 } 7 void test01(){ 8 char*p=NULL; 9 p=func(); 10 printf("%s\n",p); 11 } 12 void allocateSpace(char* p){ 13 p=malloc(100); 14 strcpy(p,"hello world!"); 15 printf("%s\n",p); 16 } 17 void test02(){ 18 char*p=NULL; 19 allocateSpace(p); 20 printf("%s\n",p); 21 }
堆分配内存API:
1 #include<stdlib.h> 2 void *calloc(size_t nmemb,size_t size); 3 功能: 4 在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存置0。 5 参数: 6 nmemb:所需内存单元数量 7 size:每个内存单元的大小(单位:字节) 8 返回值: 9 成功:分配空间的起始地址 10 失败:NULL
1 #include<stdlib.h> 2 void *realloc(void *ptr,size_t size); 3 功能: 4 重新分配用malloc或者calloc函数在堆中分配内存空间的大小。realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。 5 参数: 6 ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致 7 size:为重新分配内存的大小,单位:字节 8 返回值: 9 成功:新分配的堆内存地址 10 失败:NULL
示例代码
1 void test01){ 2 int* pl=calloc(10,sizeof(int)); 3 if(pl==NULL){ 4 return; 5 } 6 for(int i=0;i<10;i++){ 7 pl[i]=i+1; 8 } 9 for(inti=0;i<10;i++){ 10 printf("%d",pl[i]); 11 } 12 printf("\n"); 13 free(pl); 14 } 15 16 void test02(){ 17 int* pl=calloc(10,sizeof(int)); 18 if(pl==NULL){ 19 return; 20 } 21 for(int i=0;i<10;i++){ 22 pl[i]=i+1; 23 } 24 25 int* p2=realloc(pl,15* sizeof(int)); 26 if(p2==NULL){ 27 return; 28 } 29 printf("%d\n",pl); 30 printf("%d\n",p2); 31 32 //打印 33 for(int i=0;i<15;i++){ 34 printf("%d",p2[i]); 35 } 36 printf("\n"); 37 38 //重新赋值 39 for(inti=0;i<15;i++){ 40 p2[i]=i+1; 41 } 42 43 //再次打印 44 for(int i=0;i<15;i++){ 45 printf("%d",p2[i]); 46 printf("\n"); 47 free(p2); 48 }
(4)全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。
注意:
(1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。
(2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。
(3)字符串常量存储在全局/静态存储区的常量区。
示例代码
1 int v1=10;//全局/静态区 2 const int v2=20;//常量,一旦初始化,不可修改 3 static int v3=20;//全局/静态区 4 char*p1;//全局/静态区,编译器默认初始化为NULL 5 //那么全局static int和全局int变量有什么区别? 6 void test(){ 7 static int v4=20;//全局/静态区 8 }
加深理解
1 char* func(){ 2 static char arr[]="hello world!";//在静态区存储可读可写 3 arr[2]='c'; 4 char*p="hello world!";//全局/静态区-字符串常量区 5 //p[2]='c';//只读,不可修改 6 printf("%d\n",arr); 7 printf("%d\n",p); 8 printf("%s\n",arr); 9 return arr; 10 } 11 12 void test(){ 13 char*p=func(); 14 printf("ss\n",p); 15 }
字符串常量是否可修改?字符串常量优化:
ANSIC中规定:修改字符串常量,结果是未定义的。
ANSIC并没有规定编译器的实现者对字符串的处理,例如:
1.有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。
2.有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。
所以尽量不要去修改字符串常量!
C99标准:
char *p="abc";defines p with type"pointer to char"and initializes it to point to an object with type “array of char”with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array,the behavior is undefined.
字符串常量地址是否相同?
tc2.0,同文件字符串常量地址不同。
Vs2013,字符串常量地址同文件和不同文件都相同。
Devc++、QT同文件相同,不同文件不同。
8、总结
在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。
数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。
可以说,C/C++内存分区其实只有两个,即代码区和数据区。
在学习c语言提高总结了笔记,并分享出来。有问题请及时联系博主:Alliswell_WP,转载请注明出处。