C/C++ extern, static, const, volatile, auto关键字

时间:2022-09-07 15:48:35
一、extern关键字
* 声明变量/函数是在其他文件中定义的
* 在C++文件中调用C方式编译的函数
(1)声明变量/函数是在其他文件中定义的。
extern int tmp;
extern int func(int tmp);
A.cpp和B.cpp都定义了全局变量i,分别正常编译都可以通过编译,但进行链接的时候,却出现了错误。
$ gcc -E A.cpp -o A.ii   // 预编译
$ gcc -S A.ii -o A.s    // 编译
$ gcc -c A.s -o A.o     // 汇编
$ gcc -E B.cpp -o B.ii
$ gcc -S B.ii -o B.s
$ gcc -c B.s -o B.o
$ g++ A.cpp B.cpp
/tmp/cciwkLQu.o:(.bss+0x0): multiple definition of `i'
/tmp/ccCWhw8b.o:(.bss+0x0): first defined here
collect2: ld returned 1 exit status
/*  A.cpp  */
int i;
int main()
{
    return 0;
}
/*  B.cpp  */
int i;
在A.cpp中调用全局变量i,编译时也会出错。
$ g++ A.cpp B.cpp
A.cpp: In function ‘int main()’:
A.cpp:3: error: ‘i’ was not declared in this scope
/*  A.cpp  */
int main()
{
    i = 20;
    return 0;
}
/*  B.cpp  */
int i;
在A.cpp中加入声明extern int i;可以正常编译和链接。
/*  A.cpp  */
extern int i;
int main()
{
    i = 20;
    return 0;
}
/*  B.cpp  */
int i;
(2)在C++文件中调用C方式编译的函数
// B.h
void func(int *tmp);
// B.c
void func(int *tmp)
{
    *tmp += 2;
}
// A.cpp
#include <iostream>
#include "B.h"

int main()
{
    int i = 6;
    func(&i);
    std::cout<<"i = "<< i <<std::endl;
    return 0;
}
使用g++进行编译,可以正常编译没有出错。g++把C文件当作C++文件来编译,因此在链接时能够找到相应的函数。
$ g++ A.cpp B.c B.h
$ ./a.out
i = 8
如果用gcc以C的方式编译B.c,然后再编译A.cpp时,会出现找不到函数func的情况。
$ gcc -c B.c -o B.o
$ g++ A.cpp B.o B.h
/tmp/cc2MpenR.o: In function `main':
A.cpp:(.text+0x1a): undefined reference to `func(int*)'
collect2: ld returned 1 exit status
如果把头文件B.h改为如下,就可以正常编译、链接。
$ gcc -c B.c -o B.o
$ g++ A.cpp B.o B.h
$ ./a.out
i = 8
// B.h
#ifdef __cplusplus
extern "C" {
#endif
    void func(int *tmp);
#ifdef  __cplusplus
}
#endif

二、static关键字
C中引入关键字static,第一个含义:为了表示退出一个块后仍然存在的局部变量;第二个含义:用来表示不被其它文件访问的全局变量和函数。
C++重用了这个关键字,表示 属于一个类不是属于此类的任何特定对象的变量和函数。
static关键字的用法:
(1)函数体内的static变量。作用范围:该函数体。该变量在内存中只被分配一次,因此其值在下次调用时仍维持上次的值。
(2)模块内的static全局变量。作用范围:只能被模块内的所有函数访问,但不能被模块外的其他函数访问。
(3)模块内的static函数。作用范围:只能被模块内的其他函数调用。
(4)类中的static成员变量。作用范围:整个类。类的所有对象在内存中只有一份。
(5)类中的static成员函数。作用范围:整个类。static函数不接受this指针,只能访问类的static成员变量。
static全局变量与普通全局变量的区别
    static全局变量:仅在定义该变量的源文件内有效。普通全局变量:在整个源程序中有效,源程序可能由多个源文件组成。
static局部变量与普通局部变量的区别
    static局部变量:只初始化一次,下次使用时是上次的结果。普通局部变量:每次使用时都会初始化。
static函数与普通函数的区别
    static函数:在内存中只有一份。普通函数:在每个被调用中维持一份复制。
按存储区域分:全局变量、static全局变量、static局部变量都存放在内存的 静态存储区域,局部变量存放在内存的栈区。
按作用域分:全局变量在整个工程文件内都有效;static全局变量只在定义它的文件内有效;static局部变量只在定义它的函数内有效,程序分配一次内存,函数返回后该变量不会消失;局部变量在定义它的函数内有效,但函数返回后失效。
局部变量变为static局部变量,改变了它的存储方式即改变了它的生存期。全局变量改为静态全局变量,改变了它的作用域,限制了它的使用范围。

三、const关键字
1、修饰常量
const int a = 10;
int const a = 10;
2、修饰指针
int a = 10;
const int *p = &a;  // p是一个指针,指向const int 类型的变量,所以p指向的地址可以变,但指针指向的为常量
int const *p = &a;  // 同上
int* const p = &a;  // p是一个const指针,指向int类型的变量,所以p指向的地址不能变,但是指针指向的变量的值可以变。
const int* const p = &a;  // 两者都不能变
3、修饰引用
int a = 10;
const int& b = a;
int const& b = a;
4、修饰函数参数
用const修饰函数参数,传递过来的参数在函数内不可以改变。
void func(const int& n)
{
    n = 10;  // 编译错误
}
5、修饰函数返回值
const int* func()    // 返回的指针所指向的内容不能修改
{
    // return p;
}
6、修饰类成员变量
用const修饰的类成员变量,只能在类的构造函数初始化列表中赋值,不能在类构造函数体内赋值。
class A
{
public:
    A(int x):a(x)    // 正确
    {
        // a = x;    // 错误
    }
private:
    const int a;
}
7、修饰类成员函数
用const修饰的类成员函数,在该函数体内不能改变该类对象的任何成员变量,也不能调用类中任何非const成员函数
const对象只能访问const成员函数,非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。
class A
{
public:
    int& getValue() const
    {
        // a = 10;    // 错误
        return a;
    }
private:
    int a;    // 非const成员变量
}
8、修饰类对象
用const修饰的类对象,该对象内的 任何成员变量都不能被修改。
因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。
class A
{
public:
    void funcA(){}
    void funcB() const {}
};

int main()
{
    const A a;
    a.funcB();    // 可以
    a.funcA();    // 错误

    const A* b = new A();
    b->funcB();    // 可以
    b->funcA();    // 错误
}
9、在类内重载成员函数
class A
{
public:
    void func() {}
    void func() const {}  // 重载
}
10、const与宏定义的区别
const常量有数据类型,宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
const常量从汇编的角度来看,只给出了对应的内存地址;宏常量给出的是立即数。所以const定义的常量在程序运行过程中只有一份拷贝,而宏常量在内存中有若干个拷贝。
编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
#define PI 3.1415926
const double PI = 3.1415926
11、const_cast
const_cast运算符用来修改类型的const或volatile属性。
常量指针被转化成非常量的指针,并且仍然指向原来的对象;
常量引用被转化成非常量的引用,并且仍然指向原来的对象;
void func()
{
    const int a = 10;
    int *p = const_cast<int*>(&a);
    *p = 20;
    std::cout<<*p;    // 20
    std::cout<<a;    // 10

    int *pi = (int*)&a;
    *pi = 30;
    std::cout<< *p <<std::endl;    // 30
    std::cout<< a <<std::endl;    // 10
}
对于常量来说,系统没有划定专门的区域来保护其中的数据不能被更改。也就是说使用常量的方法对数据进行保护是通过编译器作语法限制来实现的。

问题1:下列赋值方法正确吗?
const A *c = new A();
A *e = c;
问题2:下列赋值方法正确吗?
A * const c = new A();
A *b = c;
问题3:下列定义赋值操作符重载函数正确吗?
const A & operator = (const A &a);
答:1、不正确,c是一个指针,执行的内容是const A,指针e指向的内容是A,如果把指针c赋值给指针e,指针e可能会更改其所指向的内容。
2、正确,c是一个const指针,执行的内容是A,指针b执行的内容为A。
3、不正确,参数列表中的const用法是正确的,赋值操作符=的左边定义为const A,在连续赋值时会出问题。
因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。
A a, b, c;
(a = b) = c;

12、const在c和c++中的区别
(1)C++中的const正常情况下是看成编译期的常量, 编译器并不为const分配空间,只是在编译的时候将其值保存在名字列表中,并在适当的时候展开在代码中。
        C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值,而且数组定义时的下标必须为常量。
下面C++代码可以通过编译并且正常运行。
// arr.cpp
#include <iostream>
using namespace std;

int main()
{
    const int a = 1;
    const int b = 2;
    int array[a + b] = {0};
    for (int i = 0; i < sizeof array / sizeof *array; i++)
        cout<< array[i] << endl;
    return 0;
}
下面C代码在编译的时候会报错:arr.c:8: error: variable-sized object may not be initialized
// arr.c
#include <stdio.h>

int main()
{
    int i;
    const int a = 1;
    const int b = 2;
    int array[a + b] = {0};   // 编译错误
    for (i = 0; i < sizeof array / sizeof *array; i++)
        //cout<< array[i] << endl;
        printf("%d", array[i]);
    return 0;
}
(2) 内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量。C/C++中内连接使用static关键字指定。
         外连接:所有被编译过的文件创建一片单独存储空间。一旦空间被创建,链接器必须解决对这片存储空间的引用。全局变量和函数使用外部连接。通过extern关键字声明,可以从其他文件访问相应的变量和函数。
C++中const默认是内连接;C中const是外连接。
C++中const对象默认为文件的局部变量。
// header.h
const int test = 1;
// test1.cpp
#include <iostream>
#include "header.h"
using namespace std;

int main()
{
    cout<< "In test1:" <<test <<endl;
    return 0;
}
//test2.cpp
#include <iostream>
#include "header.h"
using namespace std;

void print()
{
    cout<< "In test2:" <<test <<endl;
}
C++可以编译通过,如果把header.h中的内容改为extern const int test = 1;则编译失败,提示multiple definition of `test'。
$ g++ header.h test1.cpp test2.cpp

// header.h
const int test = 1;
// test1.c
#include <stdio.h>
#include "header.h"

int main()
{
    printf("In test1:%d\n", test);
    return 0;
}
// test2.c
#include <stdio.h>
#include "header.h"

void print()
{
    printf("In test2:%d\n", test);
}
C编译不过,提示 multiple definition of `test'
$ gcc header.h test1.c test2.c
(3)C++中是否为const分配空间要看具体情况,如果加上了extern或者取const变量地址,则编译器为const分配存储空间。
(4)C++中定义常量的时候不再采用define。


四、volatile关键字
变量如果加了volatile修饰,则会从内存重新装载内容,而不是从寄存器拷贝内容。
1、volatile的作用是 确保本条指令不会因为编译器的优化而省略,期望要求每次都从内存中直接读值。
#include <stdio.h>
#include <sys/timeb.h>

long long getSystemTime()
{
    struct timeb t;
    ftime(&t);
    return 1000 * t.time + t.millitm;
}

#define TIME 1000000000
int main()
{
    volatile int a, b = TIME;
    int x, y = TIME;
    long long start = 0, end = 0;
    start = getSystemTime();
    int i = 0;
    for(a = 0; a < b; a++)
        i++;
    end = getSystemTime();
    printf("volatile variable elapsed time: %lld\n", end - start);

    start = getSystemTime();
    for(x = 0; x < y; x++)
        i++;
    end = getSystemTime();
    printf("variable elapsed time: %lld\n", end - start);

    return 0;
}
通过测试可以看到for()循环语句中,如果定义的是volatile变量(a,b)编译器不会进行优化,依然会执行。如果定义的是非volatile变量(x,y)编译器会进行优化,不去执行for循环。
$ gcc test.c
$ ./a.out
volatile variable elapsed time: 3422
variable elapsed time: 1906

$ gcc -O2 test.c  // 优化编译
$ ./a.out
volatile variable elapsed time: 3418
variable elapsed time: 0
2、volatile的使用
(1)一个中断服务子程序中会访问到的非自动变量
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。
static int i = 0;    // 非自动变量
// volatile static int i = 0;
int main()
{
    while(1)
    {
        if(i)
            doSomething();
    }
}
/* Interrupt Service Routine */
void isr()
{
   i = 1;
}
程序的本意是希望isr()中断产生时,在main函数中调用doSomething()函数,但是由于编译器判读在main函数中没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判读都只使用这个寄存器里面的"i副本",导致doSomething()函数永远也不会被调用。如果将变量i加上volatile修饰,则编译器保证 对此变量的读写操作都不会被优化。
(2)多线程应用中被几个任务共享的变量
当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,防止优化编译器把变量从内存装入CPU寄存器中。
如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。
volatile让编译器每次操作该变量时一定是从内存中取出,而不是使用存在寄存器中的值。
(3)并行设备的硬件寄存器(如:状态寄存器)
存储器映射的硬件寄存器通过也要加volatile,因为每次对它的读写都可能有不同意义。
假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
int *output = (unsigned  int *)0xff800000;//定义一个IO端口; 
int init(void) 
    int i; 
    for(i=0;i< 10;i++)
    {
        *output = i;
    }
}
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为 9,
所以编译器最后给你编译编译的代码结果相当于:
int init(void)
{
    *output = 9;
}
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。
3、volatile常见问题
(1)一个参数可以既是const又是volatile吗?
可以的,例如只读的状态寄存器,它是volatile因为它可能被意想不到的改变,它是const因为程序不应该试图去修改它。
(2)一个指针可以是volatile吗?
可以的,一个中断服务子程序中修改的是指向一块buffer的指针时。
(3)下面函数有错吗
int square(volatile int *ptr)   
{   
    return *ptr * *ptr;   
}
编译器将产生类似下面的代码,由于*ptr的值可能被意想不到地改变,因此a和b的值有可能不同。
int square(volatile int *ptr)   
{   
    int a, b;
    a = *ptr;
    b = *ptr;
    return a * b;
}
正确的代码
int square(volatile int *ptr)
{
    int a;
    a = *ptr;
    return a * a;
}

五、auto关键字
C++ 98中定义的auto关键字用于声明变量为自动变量,自动变量拥有自动的生命期,此用法是多余的。
auto int b = 20 ; //拥有自动生命期
int a =10 ;  //同样拥有自动生命期
C++ 11中引入auto类型说明符,auto让编译器用过初始值来推算变量的类型。显然,auto定义的变量必须有初始值。
auto result = a + b;
若a和b都是float类型,则result的类型为float;若a和b是某个类的对象,则result也为该类的对象。
auto也能在一条语句中声明多个变量,但是基本类型必须相同。
auto i=0,*p=&i;             //正确,i是整数,p是整形指针
auto sz=0,pi=3.14;      //错误,sz和pi类型不一致