c++ 中的 extern、statuc、const以及extern "C"

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hzh_0000/article/details/79418874

在VC或VS上编写完代码,点击编译按钮准备生成exe文件时,编译器做了两步工作:
第一步,将每个.cpp(.c)和相应的.h文件编译成obj文件;
第二步,将工程中所有的obj文件进行LINK,生成最终.exe文件。
那么,错误可能在两个地方产生:
一个,编译时的错误,这个主要是语法错误;
一个,链接时的错误,主要是重复定义变量等。
编译单元指在编译阶段生成的每个obj文件。
一个obj文件就是一个编译单元。
一个.cpp(.c)和它相应的.h文件共同组成了一个编译单元。
一个工程由很多编译单元组成,每个obj文件里包含了变量存储的相对地址等。
函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可保证你的程序编译通过;
函数或变量在定义时,它就在内存中有了实际的物理空间。
如果你在编译单元中引用的外部变量没有在整个工程中任何一个地方定义的话,那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量。

  • 函数或变量可以声明多次,但定义只能有一次。

extern

  1. 作用一:当它与”C”一起连用时,如extern “C” void fun(int a, int b);,则编译器在编译fun这个函数名时按C的规则去翻译相应的函数名而不是C++的。
  2. 作用二:当它不与”C”在一起修饰变量或函数时,如在头文件中,extern int g_nNum;,它的作用就是声明函数或变量的作用范围的关键字,其声明的函数和变量可以在本编译单元或其他编译单元中使用。 即B编译单元要引用A编译单元中定义的全局变量或函数时,B编译单元只要包含A编译单元的头文件即可,在编译阶段,B编译单元虽然找不到该函数或变量,但它不会报错,它会在链接时从A编译单元生成的目标代码中找到此函数。

当有两个类都需要使用共同的变量,我们将这些变量定义为全局变量。比如,res.h和res.cpp分别来声明和定义全局变量,类ConsumerThread使用全局变量。

/**********res.h声明全局变量************/
#pragma once 

#include <QSemaphore> 

const int g_nDataSize = 1000; 
const int g_nBufferSize = 500; 

extern char g_szBuffer[];
extern QSemaphore g_qsemFreeBytes; 
extern QSemaphore g_qsemUsedBytes; 

上述代码中g_nDataSize、g_nBufferSize为全局常量,其他为全局变量。

/**********res.cpp定义全局变量************/ 
#pragma once 
#include "res.h" 

// 定义全局变量 
char g_szBuffer[g_nBufferSize]; 
QSemaphore g_qsemFreeBytes(g_nBufferSize); 
QSemaphore g_qsemUsedBytes; 
/**************************/  

在其他编译单元中使用全局变量时只要包含其所在头文件即可。

/**********类ConsumerThread使用全局变量************/
#include "consumerthread.h" 
#include "res.h" 
#include <QDebug> 
ConsumerThread::ConsumerThread(QObject* parent)
    : QThread(parent) {
}
ConsumerThread::ConsumerThread() {

}
ConsumerThread::~ConsumerThread() {
}
void ConsumerThread::run() {
    for (int i = 0; i < g_nDataSize; i++) {
        g_qsemUsedBytes.acquire();
        qDebug() << "Consumer " << g_szBuffer[i % g_nBufferSize];
        g_szBuffer[i % g_nBufferSize] = ' ';
        g_qsemFreeBytes.release();
    }
    qDebug() << "&&Consumer Over";
}
/**************************/

也可以把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如上面的

extern char g_szBuffer[]; ⇒ extern char g_szBuffer[g_nBufferSize];

然后把引用它的文件中的

#include “res.h” ⇒ extern char g_szBuffer[];

但是这样做很不好,因为你无法使用#include “res.h”(使用它,若达到两次及以上,就出现重定义错误),

  • 注:即使在res.h中加#pragma once,或#ifndef也会出现重复定义,因为每个编译单元是单独的,都会对它各自进行定义,然后在链接的时候就会出现问题

那么res.h声明的其他函数或变量,你也就无法使用了,除非也都用extern修饰,这样太麻烦,所以还是推荐使用.h中声明,.cpp中定义的做法。


static

作用一:修饰变量和函数

  • 注意:使用static修饰变量,就不能使用extern来修饰,即static和extern不可同时出现。

static修饰的全局变量的声明与定义同时进行,即当你在头文件中使用static声明了全局变量,同时它也被定义了。
static修饰的全局变量的作用域只能是本身的编译单元。在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。即在其他编译单元A使用它时,它所在的物理地址,和其他编译单元B使用它时,它所在的物理地址不一样,A和B对它所做的修改都不能传递给对方。
多个地方引用静态全局变量所在的头文件,不会出现重定义错误,因为在每个编译单元都对它开辟了额外的空间进行存储。

static 修饰的函数和修饰的变量的情况类似。
static能够进行文件隔离,static申明的变量在其他文件里extern不到,除非include

代码指示例:

res.h

/***********res.h**********/ 
static char g_szBuffer[6] = "12345"; 
void fun(); 
/************************/  

res.cpp

/***********res.cpp**********/ 
#include "res.h" 
#include <iostream> 
using namespace std; 

void fun() { 
 for (int i = 0; i < 6; i++) { 
  g_szBuffer[i] = 'A' + i; 
 } 
 cout<<g_szBuffer<<endl; 
} 
/************************/ 

然后我们在test1和test2中使用该全局静态变量:
test1.h

/***********test1.h**********/ 
void fun1(); 
/************************/  

test1.cpp

/***********test1.cpp**********/ 
#include "test1.h" 
#include "res.h" 
#include <iostream> 
using namespace std; 

void fun1() { 
fun(); 

 for (int i = 0; i < 6; i++) { 
  g_szBuffer[i] = 'a' + i; 
 } 
 cout<<g_szBuffer<<endl; 
} 
/************************/ 

test2.h

/***********test2.h**********/ 
void fun2(); 
/************************/  

test2.cpp

/***********test2.cpp**********/ 
#include "test2.h" 
#include "res.h" 
#include <iostream> 
using namespace std; 

void fun2() { 
 cout<<g_szBuffer<<endl; 
} 
/************************/  

然后我们在main中测试:
main.cpp

/***********main.cpp**********/ 
#include "test1.h" 
#include "test2.h" 

int main() { 
 fun1(); 
 fun2(); 

 system("PAUSE"); 
 return 0; 
} 
/************************/ 

运行结果:

ABCDEF
abcdef
12345

我们可以跟踪调试,发现res、test1、test2中g_szBuffer的地址都不一样。

作用二:修饰类成员变量

静态数据成员是每个 class 有一份,在全局数据区(静态区)分配内存,普通数据成员是每个 instance 有一份。

作用三:修饰类成员函数

特点:
1. 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
2. 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
3. 静态成员函数不能访问非静态成员函数和非静态数据成员;
4. 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)


const

const单独使用时,其特性与static一样(每个编译单元中地址都不一样,不过因为是常量,也不能修改,所以就没有多大关系)。
const与extern一起使用时,其特性与extern一样。

extern const char g_szBuffer[]; //写入 .h中 
const char g_szBuffer[] = “123456”; // 写入.cpp中 

const与引用

“对cosnt的引用”简称“常量引用”,但是严格来说并不存在常量引用。因为引用不是一个对象,所以没办法让引用本身恒定不变,但是c++并不允许改变引用所绑定的对象,所以从这来看引用又都算是常量。引用的对象是常量还是非常量可以决定引用所能参与的操作,但是都不会改变引用与对象的绑定关系本本身。
const 引用可以引用一个非const对象。

int i = 42;
const int &r1 = i;//正确
const int &r2 = 42;//正确
const int &r3 = r1 * 2;//正确
int &r4 = r1 * 2;//错误

const与指针

指向常量的指针:
const double pi = 3.14;
double *ptr = &pi;//错误
const double *cptr = &pi;//正确
double val = 3.14;//
cptr = &val;//正确

与常量引用一样,指向常量的指针并没有规定所指向的对象必须是一个常量。

const 指针

常量指针必须初始化,初始化完成之后他的值就不能改变了。把*放在const之前表明该指针是一个const指针。

int a = 1, b = 2;
int *c = &a, *const d = &b;
const int *e = &a, *const f = &b;

c = &b;
d = &b;//错误
e = &b;
f = &b;//错误
*c = 4;
*d = 4;
*e = 4;//错误
*f = 4;//错误

其中c e 可以变,d f不能变,*c *d 可以变,*e *f不能变,

顶层const

顶层const表示指针本身是常量,底层const表示指针所指向的对象是常量。

constexpr和常量表达式

常量表达式是指不会改变并且在编译过程就能计算结果的表达式。
字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
有的时候很难分辨一个初始值是不是常量表达式。
因此,c++11中允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是常量表达式。
声明为constexpr的变量一定是一个常量,并且一定由常量表达式来初始化。

constexpr int mf = 20;
constexpr int limit = mf + 1;

当constexpr声明指针时,紧对指针本身有效,对指针所指向的对象无关。

const int *p = nullptr;
constexpr int *q = nullptr;

p和q相差甚远,p是指向整形常量的指针,q是一个指向整型的常量指针。
因此:

constexpr int a = 0;
constexpr const int *p = &a;

constexpr函数

template<int N> struct Factorial
{
    const static int value = N * Factorial<N - 1>::value;
};
template<> struct Factorial<0>
{
    const static int value = 1;
};
// C++11  
constexpr int factorial(int n)
{
    return n == 0 ? 1 : n * factorial(n - 1);
}
// C++14  
constexpr int factorial2(int n)
{
    int result = 1;
    for (int i = 1; i <= n; ++i)
        result *= i;
    return result;
}

int main() {


    static_assert(Factorial<3>::value == 6, "error");
    static_assert(factorial(3) == 6, "error");
    static_assert(factorial2(3) == 6, "error");
    int n = 3;
    cout << factorial(n) << factorial2(n) << endl; //************
    return 0;
}
  • 以上代码演示了如何在编译期计算3的阶乘。
  • 在C++11之前,在编译期进行数值计算必须使用模板元编程技巧。具体来说我们通常需要定义一个内含编译期常量value的类模板(也称作元函数)。这个类模板的定义至少需要分成两部分,分别用于处理一般情况和特殊情况。
    代码示例中Factorial元函数的定义分为两部分:
    当模板参数大于0时,利用公式 N!=N*(N-1)! 递归调用自身来计算value的值。
    当模板参数为0时,将value设为1这个特殊情况下的值。
  • 在C++11之后,编译期的数值计算可以通过使用constexpr声明并定义编译期函数来进行。相对于模板元编程,使用constexpr函数更贴近普通的C++程序,计算过程显得更为直接,意图也更明显。
    但在C++11中constexpr函数所受到的限制较多,比如函数体通常只有一句return语句,函数体内既不能声明变量,也不能使用for语句之类的常规控制流语句。
    如factorial函数所示,使用C++11在编译期计算阶乘仍然需要利用递归技巧。
  • C++14解除了对constexpr函数的大部分限制。在C++14的constexpr函数体内我们既可以声明变量,也可以使用goto和try之外大部分的控制流语句。
    如factorial2函数所示,使用C++14在编译期计算阶乘只需利用for语句进行常规计算即可。
  • 虽说constexpr函数所定义的是编译期的函数,但实际上在运行期constexpr函数也能被调用。事实上,如果使用编译期常量参数调用constexpr函数,我们就能够在编译期得到运算结果;而如果使用运行期变量参数调用constexpr函数,那么在运行期我们同样也能得到运算结果。
    代码第 // ** 行所演示的是在运行期使用变量n调用constexpr函数的结果。
    准确的说,constexpr函数是一种在编译期和运行期都能被调用并执行的函数。出于constexpr函数的这个特点,在C++11之后进行数值计算时,无论在编译期还是运行期我们都可以统一用一套代码来实现。编译期和运行期在数值计算这点上得到了部分统一。

extern “C”

历史遗留问题,最早的标准C编译器编译引用出来的变量和函数就是在名字前面加个下滑杠,比如void foo(int a,int b);编译器引用的函数名是_foo,后来的C编译器都是遵循这个标准。
可是后来C++出现了,他的重载特性使得不同的编译器对它进行了不同的处理,比如void foo(int a,int b);就可能被编译器引用出来后变为_foo_int_int之类的,把函参也放进函数名中,从而实现了重载。为了保证不同厂家生产的模块之间的兼容性,并且可以调用原先开发好的C模块,就引出了一个extern “C”的方法,通过这种定义来告诉编译器:请按照C的方法来对我这个函数进行编译,保持我的名称。这样不同厂家的C和C++中的变量,函数,类就得到了一致化的处理,兼容性也就解决了。
这是一个标准的extern “C”的写法。这在dll的编写上作用很大。

//在.h文件的头上  
#ifdef __cplusplus  
#if __cplusplus  
extern "C"{  
 #endif  
 #endif /* __cplusplus */   
 …  
 …  
 //.h文件结束的地方  
 #ifdef __cplusplus  
 #if __cplusplus  
}  
#endif  
#endif /* __cplusplus */  

猜你喜欢

转载自blog.csdn.net/hzh_0000/article/details/79418874