跳到主要内容

变量和常量

个人理解

对象是数据类型的实例,数据类型是变量(常量)的实例.
举个例子:变量 = 自然界的一切拥有生命周期的物体(实物和虚物都可以,此时自然的常量就代表了没有生命周期的对象,比如数字、符号(文字本身可以无限存在,只不过其意义会有生命周期,但已离题太远,不再讨论))。 数据类型有人、老虎、柳树、家庭(可以看作是一个包含了几个成员变量-人的类)等,他们所占的空间和会的操作都是不同的。 对象就是具体到的每个实例化的物体。

变量

声明类、函数或变量等程序元素时,其名称只能在程序的某些部分“查看”和使用。 名称可见的上下文称为其范围。 例如,如果在函数中声明变量 x,则 x 仅在该函数正文中可见。 它具有局部范围。 程序中可能存在具有相同名称的其他变量;只要它们位于不同的范围,就不会违反“一个定义规则”,也不会引发任何错误。

对于自动非静态变量,范围还确定它们在程序内存中创建和销毁的时间。

  1. 本地变量: 定义在{}或者函数体或者构造函数中的变量. 包括命名空间范围、函数范围、语句范围、lambda范围等。
    1. 这些变量在程序进入块时创建, 在块结束时销毁.
    2. 变量的作用域就是块中.
    3. 本地变量需要强制初始化.
  2. 全局变量: 可以在程序的任何部分访问到它们. 生命周期就是程序的生命周期. 在块外声明.全局名称是在任何类、函数或命名空间之外声明的名称。 但是,在 C++ 中,即使是这些名称也具有隐式全局命名空间。 全局名称的范围从声明点扩展到声明文件末尾。 对于全局名称,可见性也受链接规则的约束,这些规则确定名称是否在程序中的其他文件中可见。
  3. 类变量: 类中的变量。1
    1. 实例变量: 在类中声明但不在任何方法和函数体中的非静态变量. 这些变量随着类的实例的创建和销毁而创建和销毁. 我们可以使用访问说明符来修饰它们. 默认为public.不需要强制初始化.
    2. 静态变量: 类变量中用 static 修饰的实例变量. 与实例变量不同,无论我们创建多少个对象,我们只能在每个类中拥有一个静态变量的副本。静态变量在程序执行开始就被创建, 在执行结束后自动被销毁. 不需要强制初始化, 因为其默认值是0. 静态变量对类的所有对象都是公共的。如果我们像访问实例变量一样访问静态变量(通过一个对象),编译器将显示警告消息,但不会停止程序。编译器会自动用类名替换对象名。 如果我们没有使用类名访问静态变量,编译器会自动添加类名(静态变量可以直接使用类名访问)。
例子
class Example
{
static int a; // static variable静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的。静态全局变量不能被其它文件所用;其它文件中可以定义相同名字的变量,不会发生冲突
int b; // instance variable
public:
func(){
int c; // local variable
}
};

int x = 0; // Global x
int y = 2;
int main()
{
// Local x
int x = 10;
static int a; // global variable 存储在静态区
cout << "value of global y is " << y << endl;
// 当存在本地变量和全局变量变量名相同时,可以通过::运算符访问全局变量.
cout << "value of global x is " << ::x << endl;
cout<< "value of local x is " << x << endl;
return 0;
}
output
value of global y is 2
value of global x is 0
value of local x is 10
全局变量和全局静态变量的区别
  1. 全局变量是不显式用 static 修饰的全局变量,全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过 extern 全局变量名的声明,就可以使用全局变量。
  2. 全局静态变量是显式用 static 修饰的全局变量,作用域是声明此变量所在的文件,其他的文件即使用 extern 声明也不能使用。

静态局部变量的特点

  1. 该变量在全局数据区分配内存;
  2. 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
  3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为 0;
  4. 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。 一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。

extern 关键字

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

常量和只读变量

只读变量(常变量):const修饰的变量,是固定值,在程序执行期间不会改变。其实际上是可读可写的,只不过写的实现需要借助于指针,存储在栈区域,如果是在函数内部定义的。
常类型量在声明之后便不可重新赋值,也不可访问其可变成员,只能访问其常成员。常成员的定义见后文。
C++ 定义了一套完善的只读量定义方法,被常量修饰符 const 修饰的对象或类型都是只读量,只读量的内存存储与一般变量没有任何区别,但是编译器会在编译期进行冲突检查,避免对只读量的修改。因此合理使用 const 修饰符可以增加代码健壮性。

只读变量和常量的区别

const字段是面向编译器的。 如果const被修改,在编译期间就会报错。 而常量是直接存在于静态内存的常量存储区。 常量:“hello”这种的才是真正的常量,存储在静态内存中的常量存储区,由系统进行管理。

const int a = 2;
a = 3;

如果修改了常量的值,在编译环节就会报错:

output
error: assignment of read-only variable‘a’
const 常量的判别准则
  • const只有用字面量初始化的 const 常量才会进入符号表
  • 使用其他变量初始化的 const 常量仍然是只读变量
  • 被 volatile 修饰的 const 常量不会进入符号表

综上所述,在编译期间不能直接确定初始值的 const 标示符,都被作为只读变量处理。

  1. 可以定义常量
const int a=100;
  1. 类型检查:const常量与#define宏定义常量的区别:~~const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查。~~感谢两位大佬指出这里问题,见:https://github.com/Light-City/CPlusPlusThings/issues/5

const 定义的变量只有类型为整数或枚举,且以常量表达式初始化时才能作为常量表达式。其他情况下它只是一个 const 限定的变量,不要将与常量混淆。

  1. 防止修改,起保护作用,增加程序健壮性
void f(const int i){
i++; //error!
}
  1. 可以节省空间,避免不必要的内存分配
    const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
备注

C++ 中类型限定符一共有三种:常量(const)、可变(mutable)和易变(volatile),其中默认情况下是可变变量,声明易变变量的情形是为了刻意避免编译器优化。

常引用和常指针

常引用和常指针也与常量类似,但区别在于他们是限制了访问,而没有更改原变量的类型。

  1. 普通指针(引用)只能指向普通变量,不能指向常变量
  2. 常指针(引用)可以指向普通变量,也可以指向常变量,但是不能修改值。
const 引用的类型与初始化变量的类型
  • 相同:初始化变量成为只读变量
  • 不同:生成一个新的只读变量
int a = 0;
const int b = 0;

int *p1 = &a;
*p1 = 1;
const int *p2 = &a;
*p2 = 2; // :x: 报错,不能通过常指针修改变量
int *p3 = &b; // :x: 报错,不能用普通指针指向 const 变量
const int *p4 = &b;

int &r1 = a;
r1 = 1;
const int &r2 = a;
r2 = 2; // :x:,不能通过常引用修改变量
int &p3 = b; // :x:,不能用普通引用指向 const 变量
const int &r4 = b;

常类型指针和常指针变量

另外需要区分开的是常类型指针和常指针变量(即常指针、指针常量),例如下列声明

int* const p1;        // 类型为int的常指针,需要初始化
const int* p2; // 类型为const int的指针
const int* const p3; // 类型为const int的常指针

int (*f1)(int); // 普通的函数指针
// int (const *f2)(int); // 指向常函数的指针,不可行
int (*const f3)(int) = some_func; // 指向函数的常指针,需要初始化
int const* (*f4)(int); // 指向返回常指针的函数指针
int const* (*const f5)(int) = some_func; // 指向返回常指针的函数的常指针

我们把常类型指针又称底层指针、常指针变量又称顶层指针。

另外,C++ 中还提供了 const_cast 运算符来强行去掉或者增加引用或指针类型的 const 限定,不到万不得已的时候请不要使用这个关键字。

常参数

在函数参数里限定参数为常类型可以避免在函数里意外修改参数,该方法通常用于引用参数。此外,在类型参数中添加 const 修饰符还能增加代码可读性,能区分输入和输出参数。

void sum(const std::vector<int> &data, int &total) {
for (auto iter = data.begin(); iter != data.end(); ++iter)
total += *iter; // iter 是 const 迭代器,解引用后的类型是 const int
}

常成员

常成员指的是类型中被 const 修饰的成员,常成员可以用来限制对常对象的修改。其中,常成员变量与常量声明相同,而常成员函数声明方法为在成员函数声明的末尾(参数列表的右括号的右边)添加 const 修饰符。

#include <iostream>

struct ConstMember {
const int *p; // 常类型指针成员
int *const q; // 常指针变量成员
int s;

void func() { std::cout << "General Function" << std::endl; }

void constFunc1() const { std::cout << "Const Function 1" << std::endl; }

void constFunc2(int ss) const {
// func(); // 常成员函数不能调用普通成员函数
constFunc1(); // 常成员函数可以调用常成员函数

// s = ss; // 常成员函数不能改变普通成员变量
// p = &ss; // 常成员函数不能改变常成员变量
}
};

int main() {
const int a = 1;
int b = 1;
struct ConstMember c = {.p = &a, .q = &b}; // 指派初始化器是C++20中的一种语法
// *(c.p) = 2; // 常类型指针无法改变指向的值
c.p = &b; // 常类型指针可以改变指针指向
*(c.q) = 2; // 常指针变量可以改变指向的值
// c.q = &a; // 常指针变量无法改变指针指向
const struct ConstMember d = c;
// d.func(); // 常对象不能调用普通成员函数
d.constFunc2(b); // 常对象可以调用常成员函数
return 0;
}

res

变量和常量在内存中的存储

在C++中,const修饰的变量是常量。 这一点和C不同。

  1. 局部变量存储在栈区.
  2. 全局变量, 静态变量和常量存储在静态区BSS中.
  3. new()或者malloc()的变量存放在堆区
备注

在C中, 静态存储区不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。

在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态存储区,静态区,常量区,常变量区,全局区,字符串常量区,静态常量区,静态变量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。 数据区包括:堆,栈,静态存储区。 静态存储区包括:常量区(静态常量区),全局区(全局变量区)和静态变量区(静态区)。 常量区包括:字符串常量区和常变量区。 代码区:存放程序编译后的二进制代码,不可寻址区。 可以说,C/C++内存分区其实只有两个,即代码区和数据区。

#include <string>

int a = 0; // a在数据段,0为文字常量,在代码段
char *p1; // BSS段,系统默认初始化为NULL
void main() {
int b; // 栈
char *p2 = "123456"; // 字符串"123456"在代码段,p2在栈上
static int c = 0; // c 在数据段
const int d = 0; // 栈
static const int d; // 数据段
p1 = (char*)malloc(10); // 分配的10字节在堆
strcpy(p1,"123456"); // "123456"放在代码段,编译器可能会将它与p2所指向的"123456"优化成一个地方
}

Storage Classes

C++ Storage Classes are used to describe the characteristics of a variable/function. It determines the lifetime, visibility, default value, and storage location which helps us to trace the existence of a particular variable during the runtime of a program. Storage class specifiers are used to specify the storage class for a variable.

C++ uses 6 storage classes, which are as follows:

storage classkeywordlifetimevisibilityinitial value
Automaticautofunction blocklocalgarbage
externalexternwhole programglobal0
staticstaticwhole programlocal0
register(deprecated in C++17)registerblocklocalgarbage
mutablemutableclasslocalgarbage
thread localthread_localwhole threadlocal or globalgarbage

auto

The auto storage class is the default class of all the variables declared inside a block. The auto stands for automatic and all the local variables that are declared in a block automatically belong to this class.

extern Storage Class

The extern storage class simply tells us that the variable is defined elsewhere and not within the same block where it is used (i.e. external linkage). Basically, the value is assigned to it in a different block and this can be overwritten/changed in a different block as well. An extern variable is nothing but a global variable initialized with a legal value where it is declared in order to be used elsewhere.

A normal global variable can be made extern as well by placing the ‘extern’ keyword before its declaration/definition in any function/block. The main purpose of using extern variables is that they can be accessed between two different files which are part of a large program.

#include <iostream>
using namespace std;

int x;
void externStorageClass()
{
extern int x;
cout << "Value of the variable 'x' declared, as extern: " << x << "\n";

x = 2;
cout << "Modified value of the variable 'x' declared as extern: " << x;
}

int main()
{
externStorageClass();
return 0;
};
output
Value of the variable 'x'declared, as extern: 0
Modified value of the variable 'x' declared as extern: 2

static

The static storage class is used to declare static variables which are popularly used while writing programs in C++ language. Static variables have the property of preserving their value even after they are out of their scope! Hence, static variables preserve the value of their last use in their scope.

We can say that they are initialized only once and exist until the termination of the program. Thus, no new memory is allocated because they are not re-declared. Global static variables can be accessed anywhere in the program.

#include <iostream>
using namespace std;

int staticFun()
{
cout << "For static variables: ";
static int count = 0;
count++;
return count;
}

int nonStaticFun()
{
cout << "For Non-Static variables: ";
int count = 0;
count++;
return count;
}

int main()
{
// Calling the static parts
cout << staticFun() << "\n";
cout << staticFun() << "\n";

// Calling the non-static parts
cout << nonStaticFun() << "\n";
cout << nonStaticFun() << "\n";
return 0;
}
output
For static variables: 1
For static variables: 2
For Non-Static variables: 1
For Non-Static variables: 1

The static keyword has different meanings when used with different types. We can use static keywords with:

Static Variables: Variables in a function, Variables in a class

  1. Static variables in a Function: When a variable is declared as static, space for it gets allocated for the lifetime of the program. Even if the function is called multiple times, space for the static variable is allocated only once and the value of the variable in the previous call gets carried through the next function call. This is useful for implementing coroutines in C/C++ or any other application where the previous state of function needs to be stored.
  2. Static Members of Class: Class objects and Functions in a class Let us now look at each one of these uses of static in detail.

static variables

#include <iostream>
#include <string>
using namespace std;

void demo()
{
// static variable
static int count = 0;
cout << count << " ";

// value is updated and
// will be carried to next
// function calls
count++;
}

int main()
{
for (int i = 0; i < 5; i++)
demo();
return 0;
}
//output: 0 1 2 3 4

Static variables in a class: As the variables declared as static are initialized only once as they are allocated space in separate static storage so, the static variables in a class are shared by the objects. There can not be multiple copies of the same static variables for different objects. Also because of this reason static variables can not be initialized using constructors.

#include <iostream>
using namespace std;

class GfG {
public:
static int i;

GfG(){
// Do nothing
};
};

int main()
{
GfG obj1;
GfG obj2;
obj1.i = 2;
obj2.i = 3;

// prints value of i
cout << obj1.i << " " << obj2.i;
}
output
undefined reference to `GfG::i'
collect2: error: ld returned 1 exit status

You can see in the above program that we have tried to create multiple copies of the static variable i for multiple objects. But this didn’t happen. So, a static variable inside a class should be initialized explicitly by the user using the class name and scope resolution operator outside the class as shown below:

#include <iostream>
using namespace std;

class GfG {
public:
static int i;

GfG(){
// Do nothing
};
};

int GfG::i = 1;

int main()
{
GfG obj;
// prints value of i
cout << obj.i;
}
output
1

Static Members of Class

Class objects as static: Just like variables, objects also when declared as static have a scope till the lifetime of the program. Consider the below program where the object is non-static.

#include <iostream>
using namespace std;

class GfG {
int i;

public:
GfG()
{
i = 0;
cout << "Inside Constructor\n";
}
~GfG() { cout << "Inside Destructor\n"; }
};

int main()
{
int x = 0;
if (x == 0) {
GfG obj;
}
cout << "End of main\n";
}
output
Inside Constructor
Inside Destructor
End of main

In the above program, the object is declared inside the if block as non-static. So, the scope of a variable is inside the if block only. So when the object is created the constructor is invoked and soon as the control of if block gets over the destructor is invoked as the scope of the object is inside the if block only where it is declared. Let us now see the change in output if we declare the object as static.

#include <iostream>
using namespace std;

class GfG {
int i = 0;

public:
GfG()
{
i = 0;
cout << "Inside Constructor\n";
}

~GfG() { cout << "Inside Destructor\n"; }
};

int main()
{
int x = 0;
if (x == 0) {
static GfG obj;
}
cout << "End of main\n";
}
output
Inside Constructor
End of main
Inside Destructor

You can clearly see the change in output. Now the destructor is invoked after the end of the main. This happened because the scope of static objects is throughout the lifetime of the program.

Static functions in a class: Just like the static data members or static variables inside the class, static member functions also do not depend on the object of the class. We are allowed to invoke a static member function using the object and the ‘.’ operator but it is recommended to invoke the static members using the class name and the scope resolution operator. Static member functions are allowed to access only the static data members or other static member functions, they can not access the non-static data members or member functions of the class.

// C++ program to demonstrate static
// member function in a class
#include <iostream>
using namespace std;

class GfG {
public:
// static member function
static void printMsg() { cout << "Welcome to GfG!"; }
};

// main function
int main()
{
// invoking a static member function
GfG::printMsg();
}
output
Welcome to GfG!

register

The register storage class declares register variables using the ‘register’ keyword which has the same functionality as that of the auto variables. The only difference is that the compiler tries to store these variables in the register of the microprocessor if a free register is available. This makes the use of register variables to be much faster than that of the variables stored in the memory during the runtime of the program. If a free register is not available, these are then stored in the memory only.

An important and interesting point to be noted here is that we cannot obtain the address of a register variable using pointers.

Properties of register Storage Class Objects

  • Scope: Local
  • Default Value: Garbage Value
  • Memory Location: Register in CPU or RAM
  • Lifetime: Till the end of its scope
#include <iostream>
using namespace std;

void registerStorageClass()
{

cout << "Demonstrating register class\n";

// declaring a register variable
register char b = 'G';

// printing the register variable 'b'
cout << "Value of the variable 'b'"
<< " declared as register: " << b;
}
int main()
{

// To demonstrate register Storage Class
registerStorageClass();
return 0;
}

mutable

Sometimes there is a requirement to modify one or more data members of class/struct through the const function even though you don’t want the function to update other members of class/struct. This task can be easily performed by using the mutable keyword. The keyword mutable is mainly used to allow a particular data member of a const object to be modified.

When we declare a function as const, this pointer passed to the function becomes const. Adding a mutable to a variable allows a const pointer to change members.

The mutable specifier does not affect the linkage or lifetime of the object. It will be the same as the normal object declared in that place.

#include <iostream>
using std::cout;

class Test {
public:
int x;

// defining mutable variable y
// now this can be modified
mutable int y;

Test()
{
x = 4;
y = 10;
}
};

int main()
{
// t1 is set to constant
const Test t1;

// trying to change the value
t1.y = 20;
cout << t1.y;

// Uncommenting below lines
// will throw error
// t1.x = 8;
// cout << t1.x;
return 0;
}
output
20
  1. const用于说明接口,使得在用指针和引用将数据传递给函数时就不必担心数据会被改变了. 编译器强制执行const做出承诺.
  2. constexpr编译时求值. 用于说明常量,允许将数据置入只读内存中来提升性能. 必须编译器计算.
constexpr int dmv = 17; //dmv is a constant
int var = 17; //var is not a constant
const double sqv = sqrt(var); //sqv is a constant

double sum(const vector<double>&); // sum won't change its arg's value

vector<double> v {1.2,3.4,4.5}; //v is not a constant
const double s1 = sum(v); //✔sum在运行时求值
constexpr double s2 = sum(v); //❌ sum v不是常量表达式

如果某个函数被用在常量表达式中,即该常量表达式在编译时求值,则这个函数必须定义为constexpr:

constexpr double square(double x){
return x*x;
}
constexpr double max1 = 1.4 * square(17); //✔
constexpr double max2 = 1.4 * square(var); //❌
const double max3 = 1.4 * square(var); //✔

constexpr函数可以接受非常量参数,但此时其结果不再是一个常量表达式. 当程序的上下文不要求常量表达式时, 我们可以使用非常量表达式参数来调用constexpr函数,这样就不用将本来相同的函数定义两次了:一次用于常量表达式,一次用于变量.

想要定义constexpr函数必须非常简单. 无副作用且仅使用通过参数传递的信息.特别是函数不能更改非局部变量,但可以包含循环以及使用自己的局部变量.

constexpr double nth(double x,int n){
double res = 1;
int i = 0;
while(i < n){
res *= n;
++i;
}
return res;
}

Footnotes

  1. 类本身就是一种变量。类可以看成是一组变量的集合。当然在不讨论其他成员函数的情况下。按我们第一个例子,

Loading Comments...