Static,const和volatile Shepard-Wang

static

  1. 将具有 外部链接 属性的变量/函数转换为 内部链接

    全局变量和函数的链接属性默认为外部链接(对其他文件可见)

    我们在 test.cpp 文件中定义全局变量global,并声明函数 func()

    test.cpp

    #include<iostream>
       
    using namespace std;
       
    void func();
       
    int global = 0;
       
       
    int main()
    {
    	func(); // 调用 test2.cpp 中定义的 func 函数
    }
    

    test2.cpp文件中定义函数func()并声明变量global

    test2.cpp

    #include<iostream>
       
    extern int global;
       
    void func()
    {
        // 使用 test.cpp 中定义的 global 变量
    	std::cout << "global int test2.cpp: " << global << std::endl; 
    }
    

    编译两个文件并运行,得到输出:

    global int test2.cpp: 0
    

    现在我们使用 static 声明函数和变量:

    static int global = 0;
       
    static void func()
    {
    	std::cout << "global int test2.cpp: " << global << std::endl;
    }
    

    编译发现报错:

    错误	LNK2019	无法解析的外部符号 "void __cdecl func(void)" (?func@@YAXXZ),函数 _main 中引用了该符号	
       
    错误	LNK2001	无法解析的外部符号 "int global" (?global@@3HA)	
    

    此时 globaltest2.cpp 是不可见的;func() 函数对 test.cpp 是不可见的

  2. 将具有 自动存储期限 的变量转换为 静态存储期限

    auto变量(函数局部变量)都是在栈内存区存放,函数结束后就自动释放。但是全局的和函数内定义的static变量都是存放在数据区的,且只存一份,只在整个程序结束后才自动释放。

    void func()
    {
        // static 类型变量只在程序第一次执行到定义时初始化
    	static int i = 0; 
    	int j = 0;
    	cout << "i = "<< i++ << " j = " << j++ << endl;
    }
    int main()
    {
    	func();
    	func();
    	func();
    }
    

    输出:

    i = 0 j = 0
    i = 1 j = 0
    i = 2 j = 0
    
  3. C++ 中 静态成员变量 :整个类共享。必须在类外定义

    class A
    {
    private:
    	int _i;
    	static int _si;
       
    public:
    	static int _hi;
    	static void print();
    	void show();
    };
       
    // 只有定义时可以使用 类名:: 访问到,其他时候不可以
    int A::_si = 10;
    int A::_hi = 20;
       
    int main()
    {
    	A a;
       
    	// 不在类外定义,会报连接错误
    	cout << A::_hi << endl;
    	cout << a._hi << endl;
    	// cout << A::_si << endl; // ERROR _si 不可访问
    }
    

    注意 :如果 _hi 的类型为 static const 那么可以直接在类内初始化

    static const int _hi = 10;
    

    静态成员函数 :只能调用静态成员函数,使用静态成员变量(没有 this 指针)

    class A
    {
    private:
    	int _i;
    	static int _si;
       
    public:
    	static  int _hi;
    	static void print();
    	void show();
    };
       
    int A::_si = 10;
    int A::_hi = 20;
       
    void A::print()
    {
    	cout << "_si = " << _si << endl;
    	// cout << "i = " << _i << endl; // ERROR 静态成员函数只能只能调用静态成员函数,使用静态成员变量
    }
       
    void A::show()
    {
    	cout << "i = " << _i << endl;
    	cout << "_si = " << _si << endl; // OK
    }
       
    int main()
    {
    	A a;
       	
    	A::print();
    	// A::show(); // ERROR 非静态成员函数不能使用 类名 + :: 访问
    	a.print();
    	a.show();
    }
    

参考文章:

https://www.jianshu.com/p/0b2d9679a9f2

static 变量的初始化顺序:

以下是 Stackoverflow 上的一个回答原文,链接在下面

Before any function in a translation unit is executed (possibly after main began execution), the variables with static storage duration (namespace scope) in that translation unit will be “constant initialized” (to constexpr where possible, or zero otherwise), and then non-locals are “dynamically initialized” properly in the order they are defined in the translation unit (for things like std::string="HI"; that aren’t constexpr). Finally, function-local statics will be initialized the first time execution “reaches” the line where they are declared. All static variables all destroyed in the reverse order of initialization.

全局作用域的 static 变量在本翻译单元任何函数执行前初始化

函数(局部)作用域的 static 变量在程序首次执行到该变量声明处出进行初始化

所有的 static 变量以初始化的相反顺序销毁。

对没有使用常量表达式初始化的 static 变量,可以将其放入函数中进行初始化:

The easiest way to get all this right is to make all static variables that are not constexpr initialized into function static locals, which makes sure all of your statics/globals are initialized properly when you try to use them no matter what, thus preventing the static initialization order fiasco.

T& get_global() {
    static T global = initial_value();
    return global;
}

参考问题:

https://stackoverflow.com/questions/15235526/the-static-keyword-and-its-various-uses-in-c?r=SearchResults

SIOF (static initialization order fiasco)问题

//file1.cpp
extern int y;
int x=y+1;

//file2.cpp
extern int x;
int y=x+1; 

问:xy 会获得什么值?

参考问题:

https://stackoverflow.com/questions/3035422/static-initialization-order-fiasco

原回答如下:

The initialization steps are given in 3.6.2 “Initialization of non-local objects” of the C++ standard:

Step 1: x and y are zero-initialized before any other initialization takes place.

Step 2: x or y is dynamically initialized - which one is unspecified by the standard. That variable will get the value 1 since the other variable will have been zero-initialized.

Step 3: the other variable will be dynamically initialized, getting the value 2.

const

《Effective C++》明确指出:尽可能地使用 const (Use const whenever possible)

1. const 与指针

char greeting[] = "Hello";
char* p = greeting;       // non-const data,non-const pointer
const char* p = greeting; // const data,non-const pointer
char const* p = greeting; // const data,non-const pointer
char* const p = greeting; // non-const data,const pointer
const char* const p = greeting; // const data,const pointer

如果关键字 const 出现在 * 左侧,表示被指物是常量;如果关键字 const 出现在 * 右侧,表示指针自身是常量。

2. STL 迭代器

STL 的 const_iterator 表示迭代器所指物不可改变(const T*):

#include<vector>

int main()
{
	vector<int> v{ 1, 2, 3 };

	const vector<int>::iterator it = v.begin();
	*it = 10; // OK
	it++;	  // ERROR

	vector<int>::const_iterator cit = v.begin();
	*cit = 20; // ERROR
	cit++;	   // OK
}

3. 修饰函数返回值和函数参数

考虑有理数的 operator* 声明式:

class Rational{...}
const Rational operator*(const Rational& lhs, const Rational& rhs);

如果不返回 const,客户就可以实现这样的暴行:

Rational a, b, c;
(a * b) = c;

将函数返回值或者参数修饰为 const,可以避免客户或函数内部修改该变量。

4.修饰函数自身(成员函数)

class TextBlock
{
public:
    ...
    const char& operator[](std::size_t pos) const
    {return text[pos];}
    char& operator[](std::size_t pos)
    {return text[pos];}
private:
    std::string text;
}

注意 non-const 函数返回类型是 reference to char,不是 char。如果返回类型是 char,下面的语句就无法通过编译:

text[0] = 'x';

如果函数返回类型是个内置类型,那么改动函数返回值从来就不合法。

bitwise constness 与 logical constness

bitwise const 阵营的人认为成员函数只有在不更改对象的任何成员变量(static 除外)时才可以说是 const。也就是不更改对象内的任何一个 bit 。请看下面的类:

#include<string.h>
class CTextBlock
{
private:
	char* _pText;
public:
	CTextBlock()
		:_pText(new char[10])
	{
		strcpy(_pText, "Hello");
	}
	char& operator[](int pos)const
	{
		return _pText[pos];
	}
};

int main()
{
	CTextBlock ctb;
	ctb[0] = 'A';
}

虽然修改了指针所指之物,但是只有指针隶属于对象,而指针所指之物并非对象成员,称此函数为 bitwise const 不会引发编译器异议。

logical constness 主张一个 const 成员函数可以修改它所处理的对象内的某些 bits,但只有客户端侦测不出的情况下才得如此。例如你的 CTextBlock class 有可能高速缓存(cache)文本区块的长度以便应付询问:

class CTextBlock
{
private:
	char* _pText;
	std::size_t _textLength; // 最近一次计算的文本区块长度
	bool _lengthIsValid;     // 目前长度是否有效
public:
	...
	std::size_t length() const;
};
std::size_t CTextBlock::length() const
{
	if (!_lengthIsValid)
	{
		_textLength = std::strlen(_pText); // ERROR
		_lengthIsValid = true;			   // ERROR 不能在 const 成员函数内修改成员变量的值 
	}
	return _textLength;
}

length 的实现不是 bitwise const,但是编译器不同意修改,坚持 bitwise const,怎么办?

解决办法:使用 mutable 释放掉 non-static 成员变量的 bitwise const 约束:

class CTextBlock
{
private:
	char* _pText;
	mutable std::size_t _textLength; // 这些成员变量总是可以被更改,即使在 const 成员函数内
	mutable bool _lengthIsValid;     
public:
	CTextBlock()
		:_pText(new char[10])
	{
		strcpy(_pText, "Hello");
	}
	std::size_t length() const;
};
std::size_t CTextBlock::length() const
{
	if (!_lengthIsValid)
	{
		_textLength = std::strlen(_pText); // OK
		_lengthIsValid = true;
	}
	return _textLength;
}

在 const 和 non-const 成员函数中避免重复

class CTextBlock
{
private:
	std::string _pText;
public:
	const char& operator[](std::size_t pos) const
	{
		...//边界检查
		...//志记数据访问
		...//检验数据完整性
		return _pText[pos];
	}

	char& operator[](std::size_t pos)
	{
		...//边界检查
		...//志记数据访问
		...//检验数据完整性
		return _pText[pos];
	}
};

可以看到 const 和 non-const 成员函数功能完全相同,唯一不同就是 const 成员函数的返回类型多了一个 const 资格修饰。代码重复会带来编译时间,维护,代码膨胀等问题。

当然,可以将 边界检查等功能移到另一个成员函数中(访问类型为 private),但是还是会重复一些代码。

这里有一种选择是 转型 ,用 non-const 成员函数调用 const 成员函数。

class CTextBlock
{
private:
	std::string _pText;
public:
	const char& operator[](std::size_t pos) const
	{
		...//边界检查
		...//志记数据访问
		...//检验数据完整性
		return _pText[pos; ]
	}

	char& operator[](std::size_t pos)
	{
		// 用 static_cast 将 *this 转换为 const CTextBlock& 后调用 const operator[] 函数
		// 否则 会调用 non-const operator 函数,引发递归
		// 然后用 const_cast 去掉 const operator[] 函数的返回值的 const 约束
		return const_cast<char&>(
			static_cast<const CTextBlock&>(*this)[pos]);
	}
};

是否可以用 const 成员函数调用 non-const 成员函数?

如果要这样调用,首先我们要使用 const_cast<CTextBlock&>(*this) 去掉 *this 的 const 性质,这是乌云笼罩的清晰前兆。

参考资料:

《Effective C++》

volatile

1、为什么用volatile?

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 “The C++ Programming Language” 对 volatile 修饰词的说明:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

2、volatile 指针

和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:

修饰由指针指向的对象、数据是 const 或 volatile 的:

const char* cpch;
volatile char* vpch;

注意:对于 VC,这个特性实现在 VC 8 之后才是安全的。

指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

char* const pchc;
char* volatile pchv;

注意:

  • (1) 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。

3、多线程下的volatile

有些变量是用 volatile 关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入 CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:

volatile  BOOL  bStop  =  FALSE;

(1) 在一个线程中:

while(  !bStop  )  {  ...  }  
bStop  =  FALSE;  
return;    

(2) 在另外一个线程中,要终止上面的线程循环:

bStop  =  TRUE;  
while(  bStop  );  //等待上面的线程终止,如果bStop不使用volatile申明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。

4、volatile 与 const 是否可以同时使用?

比如 const volatile int i 这样的声明 是否 有问题呢?

从语义上讲,似乎不可能,但是 ,这样的声明 其实是合法的。

因为 constvolatile 这两个类型限定符并不矛盾。const 表示(运行时)常量语义:被 const 修饰的对象在所在的作用域无法进行修改操作,编译器对于试图直接修改 const 对象的表达式会产生编译错误。

volatile 表示“易变的”,即 在运行期对象可能在当前程序上下文的控制流以外被修改(例如多线程中被其它线程修改;对象所在的存储器可能被多个硬件设备随机修改等情况):被volatile修饰的对象,编译器不会对这个对象的操作进行优化。\

一个对象可以同时被 constvolatile 修饰,表明这个对象体现常量语义,但同时可能被当前对象所在程序上下文意外的情况修改。

程序 1 因为没有将 i 定义为 volatile ,虽然 i 在内存中的值已经被修改,但是,输出的 i 依然没有变化!

int main()
{
    const int i = 3;
    int* pi = (int*)&i;
    *pi = 4;
    cout << i << endl;

    return 0;
}
// 输出 3

i 前加上 volatile 限定符

int main()
{
    volatile const int i = 3;
    int* pi = (int*)&i;
    *pi = 4;
    cout << i << endl;

    return 0;
}
// 输出 4

参考文章: