内存四区、全局变量、静态局部变量、extern"C"
文章目录
内存四区
- 堆区
- 栈区
- 代码区
在程序执行前,有几个内存分区已经确定。虽然分区确定,但是没有加载内存,程序只有运行时才加载内存。
text(代码区):只读,函数
data: 初始化的数据,全局变量,static变量, 文字常量区(只读)
bss: 没有初始化的数据, 全局变量,static变量
当运行程序,加载内存,首先根据前面确定的内存分区先加载
text(代码区):只读,函数
data: 初始化的数据,全局变量,static变量, 文字常量区(只读)
bss: 没有初始化的数据, 全局变量,static变量
stack(栈区):普通局部变量,自动管理内存,先进后出的特点
heap(堆区):手动申请空间,手动释放;整个程序结束,系统也会自动回收,如果没有手动释放,程序也没有结束,这个堆区空间不会自动释放
在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
与内存分配的几个函数
字符串遇到\0
就结束,strcpy不能把\0
后面的内容拷贝进去
strcpy() strncpy() strcmp()
vs
memset()
memcpy()
memmove()
memcmp()
1) memset()
头文件:#include <string.h>
void* memset(void* s ,int c ,size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
s:需要操作内存s的首地址
c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255,是以字符处理的
n:指定需要设置的大小
返回值:s的首地址
#include <stdio.h>
#include <string.h>
int main()
{
int b[10] = {0};
//处理一些代码,把b内部的元素改了
//b[10] = {0}; //err
int i = 0;
int n = sizeof(b)/sizeof(b[0]);
for(i = 0; i < n; i++)
{
b[i] = 0;
}
memset(b, 0, sizeof(b) );
char str[10];
memset(str, 'a', sizeof(str) );
for(i = 0; i < 10; i++)
{
printf("%c, ", str[i]);
}
printf("\n");
return 0;
}
int main01(int argc, char *argv[])
{
int a;
memset(&a, 0, sizeof(a) ); //常用
printf("a = %d\n", a);
//中间参数虽然是整型,但是以字符处理
memset(&a, 97, sizeof(a) );
printf("a1 = %c\n", a);
int b[10];
memset(b, 0, sizeof(b));
memset(b, 0, 10 * sizeof(int));
return 0;
}
2) memcpy()
不管有没有0,指定多长就拷贝多长,不会因为\0
提取结束。
#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 <string.h>
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int b[10];
//第3个参数是指拷贝内存的总大小
memcpy(b, a, 10 * sizeof(int) );
memcpy(b, a, sizeof(a) );
//使用memcpy()最好别出现内存重叠
//如果出现内存重叠,最好使用memmove
//memcpy(&a[2], a, 5*sizeof(int) ); //err
memmove(&a[2], a, 5*sizeof(int) );
return 0;
}
int main01(int argc, char *argv[])
{
char p[] = "hello\0mike"; //以字符串初始化,自动默认隐藏一个结束符'\0';
//sizeof显示10个
char buf[100];
printf("sizeof(p) = %lu\n", sizeof(p));
strncpy(buf, p, sizeof(p));
printf("buf1 = %s\n", buf);//
printf("buf2 = %s\n", buf + strlen("hello") + 1);
memset(buf, 0, sizeof(buf) );
memcpy(buf, p, sizeof(p));
printf("buf3 = %s\n", buf);
printf("buf4 = %s\n", buf + strlen("hello") + 1);
return 0;
}
3) memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。
4) memcmp()
#include <string.h>
int memcmp(const void*s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
s1:内存首地址1
s2:内存首地址2
n:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
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, 11};
int flag = memcmp(a, b, 10 * sizeof(int) );
if(flag < 0)
{
printf("a < b\n");
}
else if(flag > 0)
{
printf("a > b\n");
}
else
{
printf("a == b\n");
}
return 0;
}
堆区指针:malloc、new
成功返回堆区空间首元素指针,失败返回NULL
要拿free释放
手动申请空间,手动释放;整个程序结束,系统也会自动回收,如果没有手动释放,程序也没有结束,这个堆区空间不会自动释放
//new与malloc的用法
//C语言中
int *p=(int *)malloc(sizeof(int));
int (p!=NULL){
free(p);
p=NULL;
}
C++中
int *p=new int;
int (p!=NULL){
delete p;
p=NULL;
}
开辟数组
int *p=new int[10];//开辟了数组 。new int(100)是开辟了一个整数的空间,初始化为100
int (p!=NULL){
delete[] p;
p=NULL;
}
int *p=new Student;//开辟了数组,触发无参构造
int *p=new Student(10,20);//开辟了数组
int *p=new Student[10]{{},{}};//开辟了数组
int (p!=NULL){
delete[] p;
p=NULL;
}
malloc安排的空间可以用delete释放
new出来的空间可以用free释放
new和malloc区别:
- new会触发class的构造函数
- 头文件:malloc free是函数,标准库 stdlib.h;new和delete是关键字 没有头文件
- 参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
- 返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
-
自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
new在堆上舒适化一个对象的时候,会触发对象的构造函数
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
-
重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
- 内存区域
new操作符从*存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。*存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为*存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。*存储区不等于堆,如上所述,布局new就可以不位于堆中。
堆区越界:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *p = NULL;
p = (char *)malloc(0);
if(p == NULL)
{
printf("分配失败\n");
return 0;
}
strcpy(p, "mikejiang");// 错误,没有这么多空间,但是不报错
printf("%s\n", p);
free(p);
return 0;
}
栈区指针:int a;int *p=&a;
作用域
代码作用域
函数作用域
文件作用域
- 普通局部变量
大括号{}内定义的变量是局部变量。只要执行到定义变量的这个语句时候,系统才会给变量分配空间。{}的普通局部变量,加不加auto关键字等价,普通局部变量也加自动变量。不同的{}中,变量名字可以相同。
main(){
{ int a =10;
}
a=11;//报错未定义
}
main(){
int a=11;//报错未定义
{ int a =10;
printf(“%d”,a);//就近原则
}
}
- static局部变量
static局部变量,是在编译阶段就已经分配空间,函数没有调用前,就可以存在
void func(){
static int a =0;
a++;
printf("%d\n",a);
}
int main(){
func();
func();
return 0;
}
1、普通局部变量
//1、在{}内部定义的变量就是局部变量
//2、只有执行到定义变量的这个语句,系统才会给这个变量分配空间
//3、当离开{},这个非static局部自动释放
//4、局部变量的作用域在当前的{},离开此{},无法使用此变量
//5、{}的普通局部变量,加不加auto关键字等价,普通局部变量也加自动变量
//6、不同的{}中,变量名字可以一样,可以把{}类比房子,不同房子可以有同名的小伙伴
//7、普通局部变量不初始化,它的值为随机数
2、static局部变量
放在data区
//1、在{}内部定义的变量就是局部变量
//2、static局部变量,是在编译阶段就已经分配空间,函数没有调用前,它已经存在。所以int a=10;static int j=a;
是错误的
//3、当离开{},static局部变量不会释放,只有程序结束,static变量才自动释放
//4、局部变量的作用域在当前的{},离开此{},无法使用此变量
//5、不同的{}中,变量名字可以一样,可以把{}类比房子,不同房子可以有同名的小伙伴
//6、如果static局部变量不初始化,它的值默认为0
//7、static局部变量初始化语句,只会执行一次,但是可以赋值多次
//8、static变量只能用常量初始化(注意)
普通局部变量和static局部变量区别:
(1)、内存分配和释放
a)普通局部变量只有执行到定义变量的语句才分配空间
b)static局部变量在编译阶段(函数还没有执行),变量的空间已经分配
c)普通局部变量离开作用域{},自动释放
d)static局部变量只有在整个程序结束才自动释放
(2)、初始化
a)普通局部变量不初始化,值为随机数
b)static局部变量不初始化,值为0
c)static局部变量初始化语句只有第一次执行时有效
d)static局部变量只能用常量初始化
3、普通全局变量(外部链接)
普通全局变量别的.c也能使用,只要在【其他.c文件加上extern】。而且其他.c文件不能有同名定义(跟extern无关)。
//1、在{}外面(函数外面)定义的变量为全局变量(不能在main内)
//2、只有定义了全局变量,任何地方都能使用此变量
//3、如果使用变量时,在前面找不到此全局变量的定义,需要声明后才能使用extern int a;
,
//4、全局变量不初始化,默认赋值为0
//5、声明只是针对全局变量,不是针对局部变量
//6、全局变量只能定义一次,可以声明多次
//7、全局变量在编译阶段已经分配空间(函数没有执行前),只有在整个程序结束,才自动释放。这个程序的定义是与其同时编译的.c文件。
//8、不同文件,普通全局变量只能定义一次,可以声明多次,不如在两个.c文件里都有普通全局变量int a=0;
,则显示重复定义。(所以.h文件不要放定义,加#pragma once也不行。因为它作用的是同一个.c文件)
全局变量与全局静态变量的区别:
1.若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
2.若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。
3.具有外部链接的静态,可以在所有源文件里调用,除了本文件,其他文件可以通过extern的方式引用。
//test.c
int a=0;
int b=0;
void test(){
a=1;
b=2;
}
//=====================
//main.c
extern void test();//声明函数,extern可有可无
extern int a;//这样就和头文件没关系了
int main(){
test();
return 0;
}
extern int a; //声明一个全局变量 //extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。
int a; //定义一个全局变量
extern int a = 0;//定义全局变量并给初值
int a = 0; //定义全局变量并给初值
上面的四个只有第一个extern int a才是声明,其他的全是定义。
即有extern时,后面没等号时为声明,有等号就变成定义了。
当你要引用一个全局变量时,你就要声明extern int a;这个时候extern不能省,否则就成定义了。
静态局部变量和普通全局变量的区别
静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
根据静态局部变量的特点, 可以看出它是一种生存期为整个源文件的量。【虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用, 而且保存了前次被调用后留下的值。】 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用(尤其是短名全局变量的变量名污染很严重),因此仍以采用局部静态变量为宜。
简单的说静态局部变量与全局变量最明显的区别就在于:全局变量在其定义后所有函数都能用,但是静态局部变量只能在一个函数里面用。
普通全局变量的定义和声明:
//1、定义一个全局变量,建议初始化
int a = 10;
//2、如果声明一个全局变量,建议加extern
extern int a;
#ifndef _A_H
#define _A_H
//定义一个全局变量
int a = 10;
#endif
反正头文件重复包含#pragma once:同一个文件包含n次头文件,这个文件只有一次有效。而不是作用于多个文件。
4、static静态全局变量(内部链接)
a)static全局变量和普通全局变量的区别就是作用域不一样(文件作用域)
b)extern关键字只适用于普通全局变量,加了static不能用
c)普通全局变量,所有文件都能使用,前提需要声明
d)static全局变量只能本文件使用,别的文件不能使用
c)不同文件只能出现一个普通全局变量的定义
d)一个文件只能有一个static全局变量的定义,不同文件间的static全局变量,就算名字相同,也是没有关系的2个变量
普通函数和static函数的区别:
a)所有文件只能有一次普通函数的定义
b)一个文件可以有一个static函数的定义
c)普通函数所有文件都能调用,前提是使用前声明
d)static函数只能在定义所在的文件中使用
程序VS当前文件
类型 | 作用域 | 生命周期 |
---|---|---|
auto变量 | 一对{}内 | 当前函数 |
static局部变量 | 一对{}内 | 整个程序运行期 |
extern变量 | 整个程序 | 整个程序运行期 |
static全局变量 | 当前文件 | 整个程序运行期 |
extern函数 | 整个程序 | 整个程序运行期 |
static函数 | 当前文件 | 整个程序运行期 |
register变量 | 一对{}内 | 当前函数 |
extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。
extern “C”
extern “C” 的含义
例:extern "C" add()
,其中extern "C" 可以分解为extern 和 "C"
,extern标记的是“来自外部”,"C"标记的是“来自C语言语法函数”。下面详细解释:
- extern:是C/C++中的关键字,代表后面跟的是全局变量或全局函数,即声明的变量或函数是在其他.c文件中定义的全局变量或函数。如果一个a.c文件中的func函数或全局变量val想给其他b.c文件使用,那么其他文件b.c只需要#include"a.h"即可,因为a.h头文件中对函数func或全局变量val进行了extern声明。这样,在编译阶段,虽然b.c文件中没有func和val的定义,也不会报错。因为在链接阶段会从a.c编译生成的目标代码中找到func函数或val变量。
- "C"指的是:在extern的基础上,大写"C"指的是这个函数是按照C语言方式编译和链接的。extern "C"这用用法解决的是C++中的函数重载,因为C++函数重载会使函数名在编译时发生变化,比如加上一些函数形参的信息。
extern "C"解决的问题:
C和C++编译器对函数名字的处理方式是不一样的;其次,就是同为C编译器的两个不同产品,在编译时对函数名字的处理方式也是有区别的,比如microsoft vc++与dev c++。所以,extern "C"与.def文件正是为了解决这两种情况而引入的处理方法。
1: A.c文件要使用B.cpp文件中的函数func
此时在A.C中#include"B.h",但是编译链接时报错:“链接错误,未决的外部符号...”
原因在于编译器编译A.c时,会寻找C语言编译语法的函数func,而func是按C++语法命名的。所以链接的时候A.c会找不到func,c++编译的函数因为函数重载机制可能已经变名为`aaa@qq.com@rrr`,所以A.c还想寻找func函数时候自然找不到了。
解决方案:在编写C++函数时,在头文件的原有声明的函数前加上extern "C",标明某些函数要按C语言语法编译函数名。
2:B.CPP文件要调用C中的函数,同理会因为函数更名机制找不到所需函数。此时告诉B.CPP文件某些外面的函数是C语言生成的即可。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
例:
//A.h
int func(int,int);
//A.c
#include"A.h"
int func(int a,int b){
return a+b;
}
//B.h
#ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件,
extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译
#endif
#include"moduleA.h" //或者只是int func(int,int);//即可以包含头文件,也可以声明具体函数
...//其他代码,长度不限制
#ifdef __cplusplus
}
#endif
//B.cpp
#include"moduleB.h"
int main()
{
cout<<fun(2,3)<<endl;
}
这样通过extern "C"就实现了C和C++的混合编程。
注:
- 不可以将extern “C” 添加在函数内部
- 如果C++调用一个C语言编写的.DLL时,在包含.DLL的头文件或声明接口函数时,应该也要加上extern “C”。
第二、.def文件的作用(仅与VC++编程相关)
前面提到,不同厂商开发的两个C编译器也会有一些差异,最突出的就是microsoft的C编译器,它对函数名字的处理很特别(究竟是什么样子,可以使用Dumpbin工具查看dll的导出函数),所以要在使用他方编写的库时,程序链接能成功,有两种方法:1使用库编写者使用的C编译器(这里指VC++),显然这种方法不合理;2库的编写者在使用VC++编写库时使用.def文件。
.def文件的作用即是,告知编译器不要以microsoft编译器的方式处理函数名,而以指定的某方式编译导出函数(比如有函数func,让编译器处理后函数名仍为func)。这样,就可以避免由于microsoft VC++编译器的独特处理方式而引起的链接错误。
参考文章:
http://blog.chinaunix.net/u/29619/showart_230148.html
http://blog.csdn.net/weiqubo/archive/2009/10/16/4681813.aspx
http://hi.baidu.com/sundl2268/blog/item/4969453d2258bae53c6d970a.html
附录:图示
首先建了一个工程,主函数在cpp文件里,另外用c和c++语言各自实现了一个加法函数add。注意两个源文件的后缀,一个为“.c”,一个为“.cpp”。.c文件中函数名为add,.cpp文件中函数名为addCplus。工程目录文件如下:
主函数如下:
#include <stdio.h>
#include <iostream>
#include "cfunction.h"
#include "cplusfunction.h"
int main(int argc, char* argv[])
{
std::cout<<add(4, 5)<<std::endl;
system("pause");
return 0;
}
我们在c++的main函数里调用c实现的整型加法函数,结果编译时报错如下:
其中,__cdecl是C和C++默认的调用约定,这个与主题无关。
这个报错的意思是:add这个函数在被编译器编译后函数名称变成了aaa@qq.com@aaa@qq.com这个符号,而在链接的过程中,main函数去找这个符号的时候找不到。
下面我们给add这个函数加上extern “C”。 如下
//cfunction.h
//写法一,即如果当前编译单元是c++代码,那么add这个函数就会在extern “C”里声明
#ifdef __cplusplus
extern "C"{
#endif
int add(int num1, int num2);
#ifdef __cplusplus
};
#endif
//写法二
extern "C" int add(int num1, int num2);
再编译则成功。
为什么加上extern “C”就可以了呢?
我们首先要知道,函数和变量在编译后它们的名称都会转换成一个唯一符号,然后在链接的过程中,调用者根据这个唯一的符号去调用,这种符号是按照一定约定生成的。(详情参考《程序员的自我修养》第三章。)
windows下的编译器会在c语言符号(变量和函数)前加上下划线“_”,即add函数被编译后的符号为_add。
下面来验证一下。
把C实现的add的定义注释掉,再编译就能看到add被编译后的符号。
报错信息如下
可以看到,符号为“_add”。与之前的报错不同。
Vs编译器下,c++函数编译后的符号规则可参考《程序员的自我修养》第三章。
因此,当我们在c++的编译环境中调用c实现的函数时。在编译过程中,c函数add编译后的符号为“_add”,而在c++的main中,add被编译为“aaa@qq.com@aaa@qq.com”。两个符号不一致,所以就会在链接过程中报错。
一旦将add在extern “c”中声明,在c++的main中,add就被编译为“_add”。这样符号一致,链接器就能找到了。
利用extern “c”使c兼容c++的做法很常见,尤其是在c的库文件中。我们可以打开“stdio.h”这个头文件查看一下。
这样一来,在我们这个工程里调用这个c的库函数就不会报错了。