内存四区、全局变量、静态局部变量、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是关键字 没有头文件
  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++编译的函数因为函数重载机制可能已经变名为`_func@yyy@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。工程目录文件如下:

img

主函数如下:

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

img

其中,__cdecl是C和C++默认的调用约定,这个与主题无关。

这个报错的意思是:add这个函数在被编译器编译后函数名称变成了?add@@YAHHH@Z这个符号,而在链接的过程中,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被编译后的符号。

img

报错信息如下

img

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

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

img

因此,当我们在c++的编译环境中调用c实现的函数时。在编译过程中,c函数add编译后的符号为“_add”,而在c++的main中,add被编译为“?add@@YAHHH@Z”。两个符号不一致,所以就会在链接过程中报错。

一旦将add在extern “c”中声明,在c++的main中,add就被编译为“_add”。这样符号一致,链接器就能找到了。

利用extern “c”使c兼容c++的做法很常见,尤其是在c的库文件中。我们可以打开“stdio.h”这个头文件查看一下。

img

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值