C++之static、const、constexpr、volatile关键字

文章目录

  • 1.static关键字
    • 1.1 static全局变量
    • 1.2 static局部变量
    • 1.3 static函数
    • 1.4 类的static成员
    • 1.5 类的static数据成员
    • 1.6 类的static函数成员
  • 2.const关键字
    • 2.1 初始化与const、修饰普通类型的变量
    • 2.2 const与文件有效期
    • 2.3 const修饰引用
    • 2.4 const修饰指针
    • 2.5 const修饰参数传递
    • 2.6 const修饰函数返回值
    • 2.7 const修饰类的数据成员
    • 2.8 const修饰类的函数成员
    • 2.9 顶层const与底层const
    • 2.10 const修饰类对象/对象指针/对象引用
    • 2.11 const函数重载提供参考
    • 2.12 使用const比使用define有一下几种好处
    • 2.13 const与动态分配
  • 3.constexpr关键字
    • 3.1 constexpr变量
    • 3.2 constexpr函数
  • 4.volatile关键字
    • 4.1 volatile定义
    • 4.2 volatile与指针
    • 4.3 volatile与多线程
    • 4.4 volatile使用的情况

1.static关键字

1.1 static全局变量

  • 定义:在全局变量前加上关键字static,全局变量就定义成一个全局static变量。内存中的位置:全局存储区(静态存储区),在整个程序运行期间一直存在。
  • 初始化:未经初始化的全局static变量会被自动初始化为0
  • 作用域:全局stacit变量在声明他的文件之外是不可见的,准确地说作用于是从定义之处开始,到文件结尾
  • 与全局变量区别全局static变量在定义该变量的当前源文件内有效,在同一源程序的其它源文件中不能使用它。而普通的全局变量在各个源文件中都是有效的(当一个源程序由多个源文件组成时)。

1.2 static局部变量

  • 定义:在局部变量之前加上关键字static,局部变量就成为一个局部static变量。内存中的位置:全局/静态存储区。
  • 初始化:未经初始化的static局部变量会被自动初始化为0
  • 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部static变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
  • 与局部变量区别static局部变量存放在静态存储区,其只在当前函数中起作用,但是其生存周期是直到当前程序结束为止;局部变量存在中,声明周期为当前函数。

1.3 static函数

  • 定义:在函数返回类型前加static,函数就定义为static函数。函数的定义和声明在默认情况下都是extern的,但static函数只是在声明他的文件只在当前可见,不能被其他文件所用。函数的实现使用static修饰,那么这个函数只可在本文件内使用,不会同其他文件中的同名函数引起冲突。
  • 使用规则:一般情况下,在头文件中声明非static全局函数,在cpp内声明static全局函数。因为如果你需要要在多个cpp中使用某个函数,才把该函数的声明提到头文件里去,此时如果声明static全局函数,相当于只在本文件有效,没有达到预期的效果;同理,我们希望cpp文件中static全局函数只在本文件有效,就要设置为static。
  • 与普通函数区别:static函数只在当前源文件中起作用,在其他文件中不起作用;普通函数可在其他源文件中调用。static函数与普通函数最主要区别是static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝程序的局部变量存在于堆栈中

1.4 类的static成员

  • 定义:在类中static数据成员可以实现多个对象之间的数据共享,并且使用static数据成员还不会破坏隐藏的原则,即保证了安全性。因此,static数据成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,static数据成员只存储一处,供所有对象共用。
  • 特性1: static成员变量属于整个类所有,可以通过类名直接访问公有static成员变量(即使stacit数据成员是private),同时也可以通过对象名访问公有static成员变量。
  • 特性2:stacit成员变量的生命期不依赖于任何对象,为程序的生命周期
  • 特性3:c++中父类的static成员变量/函数在子类中的使用时,子类是可以共享父类中static成员变量/函数的。当然有一个前提,父类中的静态成员函数/变量应该为public,或者为private时应提供公有的接口来调用。

1.5 类的static数据成员

  • 特性1:static数据成员需要在类外单独分配空间(此时称为定义性声明,与类中的引用性声明注意区别,此时static数据成员不占用类的空间),static数据成员在程序内部位于全局静态区
  • 特性2:即使在类中已经声明static数据成员,但是依然需要在类外定义,而且不能重复使用static关键字。因为在类中声明static数据成员,不分配内存。而static变量需要开辟存储空间,所以如果不在类外部开辟空间,则会导致编译错误。
  • 特性3:static数据成员的类型可以是const、引用、指针、类类型等等。如果是const,在类外定义的时候const不可省略
  • 特性3补充:通常情况下,类的static成员不能在类的内部初始化。但是我们可以为static成员提供const整数类型的类内初始值,不过要求static成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。因为这些成员本身就是常量表达式,所以它们能用于所有适合于常量表达式的地方。还有,如果某个static成员的应用场景仅仅限于编译器可以替换它的值的情况,则初始化的const或者constexpr static不需要分别定义,否则需要分别定义。
  • 特性4:static数据成员可以是不完全类型。特别的,static数据成员的类型可以就是所属的类类型,而非static数据成员则受到限制,只能声明为它所属类的指针或引用。例如:
class Bar{
	private:
		static Bar mem1; //正确,static成员可以是不完全类型;
		Bar *mem2;       //正确,指针类型可以是不完全类型;
		Bar mem3;        //错误,数据类型必须是完全类型;
}
  • 特性5:static成员可以作为默认实参(因为static成员不属于该类),而普通成员不行。
Class Screen{
	public:
		//bk表示一个在类中的static数据成员,稍后定义;
		Screen &clear(bk=ch);
	private:
		static const char ch;
}

1.6 类的static函数成员

  • 定义:对static成员的访问不需要用对象名。在static成员函数的实现中不能直接引用类中说明的非static成员,可以引用类中说明的static成员。如果static成员函数中要引用非static成员时,必须对象来引用。例如:
Class A{
	public:
		static void f(A a);
	private:
		int x;
};
void A::f(A a){
	cout<<x;   //对x的引用是错误的;
	cout<<a.x; //正确;
}


//之所以在static函数成员访问非static成员需要指明对象,
//是因为对static成员函数的调用是没有this指针,
//因此不能像非static成员那样隐含地通过this指针访问类的非static成员;
//一般情况下,static成员函数主要用于访问同一个类中的static数据成员,
//维护对象之间的共享数据。
  • 特性1: static成员函数是类的一个特殊的成员函数,属于整个类所有,没有this指针
  • static成员函数不能声明成const:因为const修饰的对象为this指针,可以具体到某一实例,但是static成员函数没有this指针即不能实例化。
  • static成员函数不能被声明为volatile:和static成员函数不能声明成const的原因类似。
  • static成员函数不能被声明为虚函数,因为static成员不属于任何对象,所以加上virtual没有任何实际意义,又static成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的。
  • 特性2:static成员函数只能直接访问static修饰的成员变量
  • 特性3:可以通过类名直接访问类的公有static成员函数,也可以通过对象名访问类的公有和私有static成员函数。
  • 特性4:我们在定义和初始化static变量的时候也可以使用类名直接访问私有成员。例如,double Account::interestRate=initRate();,即使initRate是私有的,我们也可以用它来初始化interestRate。

2.const关键字

2.1 初始化与const、修饰普通类型的变量

  • 初始化与const:const对象一旦创建后其值就不能再改变,所以const对象必须初始化。如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要(const特征仅仅在执行改变内容的操作的时候才会发挥作用)。例如:
int i=42;
const int ci=i;  //正确,i的值被拷贝给了ci;
int j=ci;        //正确,ci的值被拷贝给了j;
  • 修饰普通类型的变量:对一个const变量赋值是违法的事情,因为 a 被编译器认为是一个const,其值不允许修改
const int  a = 7; 
int  b = a; // 正确
a = 8;       // 错误,不能改变
  • volatile关键字:如果不想让编译器察觉到程序对const的操作,我们可以在 const 前面加上 volatile 关键字,volatile 关键字跟 const 对应相反,是易变的,容易改变的意思。所以不会被编译器优化,编译器也就不会改变对 a 变量的操作。

2.2 const与文件有效期

  • 默认情况:const对象被设定为仅在单个文件内有效
  • const变量在多个文件共享:只需在一个文件中定义const,而在其他多个文件中声明并使用它;对于const变量不管是声明还是定义都添加extren关键字

2.3 const修饰引用

  • C++ 引用和指针 这篇文章的常量引用有详细介绍。

2.4 const修饰指针

  • C++ 引用和指针 这篇文章的指针常量和常量指针有详细介绍。

2.5 const修饰参数传递

  • 值传递:和其它初始化过程类似,当用实参初始化形参时会忽略顶层const,也就是形参的顶层const被忽略掉,所以实参是const对象或者非const对象都是可以的。例如:
void fun(const int i){/*fun能读取i的值,但是不能修改*/}
void fun(int i){/*...*/}//错误,上面的函数忽略掉顶层const,两者定义重复;
  • 值传递存在的问题:当使用自定义类型的值传递的时候,需要使用临时对象复制参数。对于临时对象的构造,需要调用拷贝构造函数,比较浪费时间和空间。
  • 指针传递:我们可以用非const初始化一个底层const对象。当 const 参数为指针时,可以防止指针被意外修改。例如:
void fun(int* const a)
{
    cout<<*a<<" ";
    *a = 9;
}
  • 引用传递:我们需要尽量使用const引用。第一,使用const引用函数不能修改实参的值;第二,const引用也会使形参接受更多的实参类型。例如:void fun(const Test &a){函数体}

2.6 const修饰函数返回值

  • const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
  • const 修饰自定义类型作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。通常,不建议用const修饰函数的返回值类型的情况有某个对象或对某个对象引用。原因如下:如果返回值为某个对象为const(const A test = A 实例)或某个对象的引用为const(const A& test = A 实例) ,则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。
  • const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。例如:
const int *fun2() //调用时 
const int *pValue = fun2();
//我们可以把fun2()看作成一个变量,即指针本身不可变`

int* const fun3() //调用时 
int* const pValue = fun2();
//我们可以把fun2()看作成一个变量,即指针内容(即指针所指的对象)不可变

2.7 const修饰类的数据成员

  • 定义:const修饰类的数据成员表示成员不能被修改。另外const数据成员只能在初始化列表中进行初始化。
  • mutalbe关键字:我们可以使用mutable关键字修饰这个数据成员,mutable的意思是易变的,被mutable关键字修饰的成员可以处于不断变化中。例如:
class Test{
	public:
	    Test(int a,int b):a(a),b(b){}
	    void Kf() const{
	        ++a; //错误,常成员函数不能修改普通变量的值;
	        ++b; //正确,常成员函数可以修改mutable修饰的变量;
	    }
	private:
	    int a;
	    mutable int b;
};
  • static和const都应该在类外定义:但是在C++标准规定了一个例外:类的静态常量如果具有整数类型或者枚举类型,那么可以直接在类定义中为它指定常量值。

2.8 const修饰类的函数成员

  • const 修饰类的成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 。
  • const成员函数能够访问对象的const成员,而其他成员函数不可以。
  • 这篇文章有关于const与类成员更详细的描述。

2.9 顶层const与底层const

  • 顶层const:表示指针本身是个常量。
  • 底层const:表示指针所指的对象是一个常量。
  • 当执行的对象的拷贝操作的区别顶层const不受什么影响;底层const的限制不可以忽略,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。另外,使用命名的强制类型转换函数const_cast时,只能改变底层const

2.10 const修饰类对象/对象指针/对象引用

  • const修饰类对象:表示该对象为const对象,其中的任何成员都不能被修改,对于对象指针和对象引用也是一样。const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。

2.11 const函数重载提供参考

  • void fun(int x){}void fun(int x) const{}比较, 如果仅以const来区分对成员函数重载,那么通过非const的对象调用该函数,两个重载的函数都可以与之匹配,这时编译器将选择最近的重载函数。
  • 顶层const与重载:顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和一个没有顶层const的形参区分开;
Record lookup(Phone);
Record lookup(const Phone);//错误,顶层const不影响传入函数的对象,重复声明

Record lookup1(Phone*);
Record lookup1(Phone* const);//错误,顶层const不影响传入函数的对象,重复声明
  • 底层const与重载:如果形参是某种类型的引用或者指针,则通过区分其指向的是const对象还是非const对象可以实现函数的重载,此时的const是底层的。
//对于接受引用或者指针的函数来说,对象是const还是非const对应的形参不同;
//定义了4个独立的重载函数
Record lookup(Account &);       //函数作用于Account的引用;
Record lookup(const Account &); //新函数,作用于常量引用;

Record lookup(Account *);       //新函数,作用于指向Account的指针;
Record lookup(const Account *); //新函数,作用于指向常量的指针;


//const不能转换成其他类型,
//所以我们只能把const对象(指向const的指针)传递给const形参;
//相反,因为非const可以转换成const,
//所以上面4个函数都能作用于非const对象或者指向非const对象的指针;
//此时,对于非const,编译器优先选用非const版本;
  • const_cast与重载:const_cast可以使得到的结果是普通的引用。
//此时这个函数的参数和返回类型都是const的引用;
//若对两个非const实参调用该函数,返回结果仍然是const的引用;
//因此我们需要重载函数,使得当实参不是const时,得到的结果是普通的引用;
const string &shorterString(const string &s1,const string &s2){
	return s1.size()<=s2.size()?s1:s2;
}

//使用const_cast实现;
//在这个函数中,首先将实参强制转换成const的引用,
//然后调用shorterSTring函数的const版本,const版本返回const的引用。
//这个返回的const引用事实上绑定在某个初始值的非const实参上,
//因此,我们可以再将其转换成普通的string引用是显然安全的;
string &shorterString(string &s1,string &s2){
	auto &r=shorterString(const_cast<const string &>(s1),
						  const_cast<const string &>(s2));
    return const_cast<string &>(r);
}

2.12 使用const比使用define有一下几种好处

  • 写在前面这篇文章有介绍。

2.13 const与动态分配

  • 使用new分配const对象是合法的。和其它const对象类似,一个动态分配的const对象必须初始化。例如:
//分配并初始化一个const int;
const int *pci=new const int(1024);

//分配并默认初始化一个const的空string;
const string *pcs=new const string;
  • 对于定义了默认构造函数的类类型,其const对象可以隐式初始化,而其它类型的对象就必须显式初始化
  • 虽然const对象的值不能被修改,但是它自身可以用delete销毁。
    const int *pci=new const int(1024); delete pci;//释放const对象;

3.constexpr关键字

3.1 constexpr变量

  • constexpr与普通变量:C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量是否是一个const表达式。声明为constexpr的变量一定是const,而且必须用const表达式初始化。例如:
constexpr int mf=20;       //20是常量表达式;
constexpr int limit=mf+1;  //mf+1是常量表达式;
constexpr int sz=getsize();//只有当getsize()是一个constexpr函数时
						   //才是一个正确的声明语句;
  • constexpr与引用(指针):指针和引用都能定义为constexpr,但是他们的初始值受到严格控制,比如一个constexpr指针的初始值必须是nullptr或者0,或者是存储在某个固定地址中的对象。一般来说,静态变量和全局变量的对象地址是固定不变的,能够来初始化constexpr指针。
  • constexpr与const区别:constexpr 仅对指针本身有效,与指针所指的对象无关:例如:const int *p=nullptr;//p是个指向整型常量的指针(point to const);constexpr int *q=nullptr;//q是个指向整数的指针常量(const point);它们的区别在于constexpr把它所定义的对象变为顶层const

3.2 constexpr函数

  • 规定:函数的返回类型以及形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句。一般的,为了能在编译过程中随机展开,constexpr函数被隐式转换为内联函数
  • 注意:允许constexpr函数的返回值不是一个const,也就是不一定返回常量表达式。例如:
constexpr int new_sz(){return 42;}
constexpr size_t scale(size_t cnt){return new_sz()*cnt;}

4.volatile关键字

4.1 volatile定义

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

4.2 volatile与指针

  • volatile指针和const修饰词类似,const有常量指针和指针常量的说法,volatile 也有相应的概念。修饰由指针指向的对象可以是const或volatile,例如:const char* cpch;或者 volatile char* vpch;。指针自身的值代表地址,是const或volatile:char* const pchc;或者 char* volatile pchv;
  • 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  • C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

4.3 volatile与多线程

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

4.4 volatile使用的情况

  • 中断服务程序中修改的供其它程序检测的变量需要加volatile。
  • 多任务环境下各任务间共享的标志应该加volatile。
  • 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。

你可能感兴趣的:(C++基础知识合集)