C++|怪异的const及其不可变性、可修改性、连接性

2022-12-21 0 159

const的最初意图是取代预CPU#define来展开值代替,可用作润色操作符、auth和codice、类对象及成员等。

在C中,对一些无法理解的多次重复出现的字面上值自表达式,采用#define是两个不错的选择,但即使其只是单纯的值代替,没类别检查是其被人非议之所处。

C++先驱Bjarne Stroustrup在《The design and evolution of C++》一书中详细阐释了const的来历。

“在作业系统中,常常能见到人们用两个chunk间接或间接对一块储存区展开存取控制,其中用两个位详述某一采用者若想在这儿写,另两个详述该采用者若想从这儿读。我觉得此种思想能间接加进C++中,因此也考虑过允许把两个类别叙述为readOnly或是writeOnly。”

“时至今日,在C词汇中还无法规定两个数据原素是黎贞的,换句话说,它的值必须保持不变。也没任何办法去限制表达式对传予它的模块的采用方式……readOnly操作符可用作防止对某些边线的预览。它说明,对所有出访这个边线的不合法方式而言,多于那些不发生改变储存在这儿的值的方式才是真正不合法的。”

“我在离开这次会议时得到的是同意(通过投票表决)把readOnly引入C词汇(而不是C with Classes 或是C++),但把它另外重新命名为了const。”

“const来表示物理量,能正式成为宏的一种管用代替物,但要求自上而下const帕尔霍只在它所处的校对模块起作用。即使多于在此种情况下,C++才能很容易推论出那些东西的值确实没发生改变…把单纯的const加进自表达式表达式中,并能避免为那些自表达式重新分配内部空间。”

自表达式表达式中的值不必为其初始化内部空间,与计算机程序储存在一起。C++通常不为普通const黎贞表达式重新分配储存内部空间,而是将它们保存在示例中,这使它正式成为两个校对期间的值,没了储存与读缓存的操作,使得它的工作效率也更高。

“即使C词汇中的const无法用在自表达式表达式中,这就使const在C词汇中远海控没在C++中那样管用。”

(在C中,const用来限量发行两个表达式是黎贞的,即不可变的。预设具有内部镜像,并初始化内部空间。)

1 const内部表达式的镜像性

const润色内部变量时,其codice是整个文档,预设为内部相连(即使其无法避免出现不重新分配储存内部空间,否则,众多的const在cpp文档中初始化,会导致相连错误)。

为了使const正式成为内部相连以便让另外两个文档能对它引用,必须明确把它定义成extern。extern const强制展开了内部空间重新分配,extern意味着采用内部镜像,因此必须重新分配储存内部空间(当然就要考虑多次重复定义的问题了)。

#define的值替换是放到头文档供其它cpp文档包含的,同样f ,const声明的自表达式也要能放到头文档中,头文档中只能是声明,是要避免缓存重新分配的,而C++也是这样做的,将它们保存在示例中,但当涉及到与地址相关的操作时,不得已会初始化,所以安全的做法就是内部镜像性了。这就是其独特所处,在头文档中,他看起来好像是两个定义,其实只是两个符号自表达式,还具有内部镜像性。

2 const局部表达式的copy propagation及间接修改性

const局部表达式,储存在栈区,不可间接修改,可通过操作符间接修改:

#include <iostream> using namespace std; const int gci = 55; // const自上而下表达式,自表达式区,一旦初始化不可间接或间接修改 int main() { const int ci = 2*5;// const局部表达式,栈区,可间接修改,不可间接修改 int *pi = (int*)&ci; *pi = 20;int *pgci = (int*)&gci; //*pgci = 66; // error cout << &gci << endl; // 0046F01C cout << &ci << ” “ << pi <<endl; // 0012FF44 0012FF44 cout << ci << ” “ << *pi <<endl; // 10 20 while(1); return 0; } /* 0046F01C 0012FF44 0012FF44 10 20 */

这时会发现&ci和&pi的值(地址)一样,而ci和*pi的值不一样,就是说在相同的地址下的缓存内部空间有不同的值。 ?为什么呢? ?

即使在校对期间展开了优化,即自表达式传播(copy propagation),又称自表达式替换,程序在校对过程中经过两种过程:

一是const folding,上例中const int ci = 2*5; ?计算表达式的值时,C++将2 * 5算成10,这个过程被称为const folding。

二是copy propagation,C++在遇到ci这个标识符时,自动将ci替换成10,而不是去相应地址中取值,这个过程就是copy propagation。

上例就是这两个过程的结果,在定义了const int ci后,后面代码中遇到ci的地方,C++就自动将ci替换成10。

常量折叠是在校对时间单纯化自表达式表达的两个过程。单纯来说就是将自表达式表达式计算求值,并用求得的值来替换表达式,放入自表达式表。

自表达式传播和自表达式折叠都是C++的优化方式(目的是在不影响程序正常逻辑的前提下,尽可能生成立即数寻址的目标代码,减少缓存的出访次数)。VS下开启O2优化后,会展开自表达式传播和折叠。当然,针对的是内建基本类别,而不是复合结构类别, 即使C++不会将两个复合结构类别保存到示例,所以会重新分配储存内部空间。

当采用volatile关键字润色表达式时能禁止C++这一优化,每次读都会经过寻址从原地址内部空间取得表达式当时的真实值。(所以采用这个关键字时会对性能有一定影响。)

#include <iostream> using namespace std; int main() { volatile const int a = 10; volatile int *p = (int *) &a; cout << *p << ” “ << a << endl; cout << p << ” “ << &p << endl; *p = 20; cout << *p << ” “ << a << endl; cout << p << ” “ << &p << endl; while(1); return 0; } /*output 10 10 1 0x7fff4b0a9ac0 20 20 1 0x7fff4b0a9ac0

润色表达式形参时,也可间接修改:

const int showValue(const int & f) { //f = 1000; int *p = (int*)&f; *p = 100; cout<< f <<” “ << &f<< endl; // 100 0012FECC return f; } void test() { int a = 10; showValue(a); cout << a <<” “<< &a <<endl; //100 0012FECC ,地址和上面相同 } const int showValue2(const int *f) { // *f = 1000; // error int *p = const_cast<int *>(f); *p = 200; cout << *f <<” “ << f<< endl; // 200 0012FECC return *p; } void test2() { int a = 20; showValue2(&a); cout << a <<” “<< &a << endl; //100 0012FECC ,地址和上面相同 }

3 校对时自表达式与运行时自表达式

校对时自表达式在校对后变量值已经确定,程序运行时自函值保持不变。

而运行时自表达式在程序运行时每次值都不一样。

例如:#include <iostream> using namespace std; const int cmpl = 5+1; // 校对时自表达式,不涉及储存内部空间 int read_at_runtime() { int val; cin >> val; return val; } const int rt = read_at_runtime(); // 运行时自表达式,涉及到储存内部空间 int main() { cout<<rt<<” “<<cmpl<<endl; int arr[cmpl]; //int ar2[rt]; // error while(1); return 0; }

以下几种情况无法采用运行时自表达式:

I 数组边界;

II switch case表达式;

III 位域长;

IV 枚举初始化;

V 模板的非类别模块赋值。

所以判断某一const自表达式是不是两个校对器自表达式的单纯方法就是用其声明两个静态数组:

const int size = 55; int arr[size];

4 const自表达式初始化内部空间的几种特殊情况

const将自表达式放到示例,才是两个真正意义上的自表达式,特殊情况下,如果要重新分配储存内部空间,则是声明这块储存内部空间为黎贞,此时的const就等同于CC++的做法。

在通常情况下C++是不会为const对象初始化,多于在几种情况下会重新分配地址:

4.1 extern和const同时采用,使得表达式具有了内部镜像属性。

4.2 涉及const对象地址相关操作时。此时会强制初始化地址。

4.3 runtime的const,C++是需要为它重新分配内部空间的,而且也不在示例里面记录相关信息。

4.4 对自定义数据类别 ,也会初始化,能用操作符修改值

#include <iostream> using namespace std; struct Person { char name[12];// 如果是string name;因其是聚合数据时无法间接初始化 int age; }; int main() { const Person p = {“wwu”,18}; int *ptr = const_cast<int*>(&p.age); *ptr = 18; cout<<p.age<<endl; // 28 while(1); return 0; }

5 C中的const

在C中,const润色的标识符有重新分配储存内部空间,但限量发行为黎贞,预设为内部镜像。

而在C++中,const润色的对象是否重新分配储存内部空间取决于对它如何采用。一般说来, 如果两个const仅仅用来把两个名字用两个值代替(如同采用#define一样), 那么该储存内部空间就不必创建。要是储存内部空间没创建的话(这依赖于数据类别的复杂性以及C++的性能),在展开完数据类别检查之后,为了代码更加有效, 值也许会折叠到代码中,这和以前采用#define不同。

不过, 如果取两个const的地址(甚至不知不觉地把它传递给两个带引用模块的表达式)或是把它定义成extern, 则会为该const创建缓存内部空间。

CC++无法把const看成两个校对期间的自表达式,在C中,如果写:

const int bufsize = 11; char buf[bufsize]; // error

即使bufsize占用了某块缓存,所以CC++不知道它在校对时的值,而声明静态数组需要两个校对器的自表达式。

在C中,在表达式外声明

const int bufsize;

是被允许的,即使在C中,const预设为内部镜像,如果此标识符在另外另有定义,则此处是声明,否则就是定义,被初始化为0。

在C++则不被允许,声明和定义要严格区分,怎样区分?通过初始化:

extern const int i; // 声明,表明i在另处有定义 extern const int i = 55; // extern是内部相连声明,初始化表明此处是定义 const int k = 66; // 初始化表明此处是定义,预设为内部镜像

所以在C++中,const润色的标识符,要么是extern声明,要么有初始化。

当展开了extern const声明时, 校对器就无法够展开自表达式折叠了, 即使在重新分配了储存内部空间,在校对期就不知道具体的值了。

6 const润色操作符表达式

操作符相当于表达式之间的纽带,涉及到两块缓存,有他型、他址和己址、己值,所以const润色操作符表达式时,既能润色己型,也能润色己值(操作符表达式名自身),以符号*区隔即可单纯区分润色的是哪一部分。

int x = 55; int y = 88; const int *px = &x; // const在符号*前面,润色后一部分,即他型,操作符指向的缓存区域的类别 int const *py = &y; // 需要同时初始化 //*px = 66; // error //*py = 99; // errorpx = &y; py = &x;int * const cp = &x; // const在符号*后面,润色后一部分,即己值,操作符表达式自身 //cp = &y; // error *cp =11; const int const*pp = &x;//*pp = 33; //error //pp = &6; //error int *ptr = &x; px = ptr; // ptr = px; // error, 两个const操作符表达式只能赋给两个另两个const操作符表达式; py =px;

7 char*指向两个字符串字面上量

const char* p = “hello”; char* q = “hello”;

这两种写法表达的意思都是两个指向hello这个自表达式字符串的char操作符。从C词汇时代起后一种写法就是如此,而到了C++时代,为了兼容以前的程序所以做了同样的规定,但是const char*此种写法相对而言更规范。

既然是自表达式字符串,自然其内容无法被修改,无法有以下写法:

p[2] = i; q[2] = j;

如果想修改,定义两个字符数组好了:

char str[] = “hello”; // str是容纳”hello”的起始地址,能是栈区,也能是自上而下区

8 const润色auth和codice

对const润色操作符或引用模块,这很好理解,表明是传址,但不预览其值。返回操作符或引用也是如此,只是如果是返回const操作符或引用,在做右值时,其值也需具有const属性。

但对润色两个传值的模块,只是表明这个初值不做预览。

如果是按值返回,返回的是两个内建类别的值,则用const润色没什么意义。

如果按值返回的是两个自定义类别,则用const限量发行其黎贞属性。

需要注意的是,用用两个表达式调用做实参时,会产生两个临时对象,这样的临时对象,如果是两个const润色的引用形参是能接受的,而const润色的操作符形参才不被接受,原因是操作符需要两个确切的地址,这是临时对象所欠缺的。为什么需要引用形参用const润色其引用类别呢?即使临时对象自动赋予了const属性,C++C++在做类别检查时,如果右值具有const属性,则要求左值也是两个const:

void f(int&) {} void g(const int&) {} class X {}; char str[] = “hello”; // str是容纳”hello”的起始地址,能是栈区,也能是自上而下区 X f() { return X(); } // Return by value void g1(X&) {} // Pass by non-const reference void g2(const X&) {} // Pass by const reference int main() { // Error: const temporary created by f(): //! g1(f()); // OK: g2 takes a const reference: g2(f()); //! f(1); // Error,temporaries automatically const g(1); } ///:~

9 const与类

const能修饰两个类对象,表明此类对象无法预览对象的状态,也无法调用成员表达式去预览对象的状态,如何确保这一点呢?就是将成员表达式声明为const,const放在成员表达式标识符的后面(放到前面则是润色表达式返回类别了)来做润色。

由此,const成员表达式也不同预览对象的数据成员,也无法调用非const成员表达式。对const类对象,如果某一数据成员想让const成员表达式预览,怎么办?用mutable去润色这个数据成员。还有一种变通的方法是通过const操作符去更改:

static_cast<ClassName*>(this))->memVar++;

const数据成员只是在实例化后的对象中具有 const属性,不同的对象能是不同的值。const润色的数据成员在初始化列表中初始化为不同的值,当然也只能在初始化列表中初始化。

校对期间类里的自表达式:

class StringStack { //enum{size = 100}; static const int size = 100; const string* stack[size]; int index; public: StringStack(); void push(const string* s); const string* pop(); }

10 const VS enum和#define

在C++中,自表达式有3种表达方式:cosnt、enum和#define,这3种方式有所不同:

#define在校对预处理时展开数据替换,没储存内部空间;

enum和const是类别安全的,而#define不是;

多于enum和#define能被用作 switch。

小结一下:

C++|怪异的const及其不可变性、可修改性、连接性

ref

Bruce Eckel, Chuck Allison:C++编程思想(上卷)

-End-

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务