欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

内存四区、全局变量、静态局部变量、extern"C"

程序员文章站 2024-01-26 10:11:25
...

内存四区

  • 堆区
  • 栈区
  • 代码区

在程序执行前,有几个内存分区已经确定。虽然分区确定,但是没有加载内存,程序只有运行时才加载内存。

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是关键字 没有头文件
  1. 参数

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

  1. 返回类型

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型

  1. 分配失败

new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

  1. 自定义类型

    new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

    new在堆上舒适化一个对象的时候,会触发对象的构造函数

    malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

  2. 重载

C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

  1. 内存区域

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。工程目录文件如下:

内存四区、全局变量、静态局部变量、extern"C"

主函数如下:

#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实现的整型加法函数,结果编译时报错如下:

内存四区、全局变量、静态局部变量、extern"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被编译后的符号。

内存四区、全局变量、静态局部变量、extern"C"

报错信息如下

内存四区、全局变量、静态局部变量、extern"C"

可以看到,符号为“_add”。与之前的报错不同。

Vs编译器下,c++函数编译后的符号规则可参考《程序员的自我修养》第三章。

内存四区、全局变量、静态局部变量、extern"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”这个头文件查看一下。

内存四区、全局变量、静态局部变量、extern"C"

这样一来,在我们这个工程里调用这个c的库函数就不会报错了。