const 之 指针 篇
常量指针 / 常指针
常量指针本质上是指针,表示指向常量的指针(变量)。
一般“常指针”也指常量指针。
声明方式如下:
const T *A;
orT const *A;
const修饰指向的对象,指针A可变,A指向的对象不可变。
所谓指向的对象不可变,指的是不能通过该指针去修改指向对象的内容,性质上等同于一个只读指针。但是可以通过所指向对象本身去修改内容。
比如:
int a;
iut c;
cosnt int *b = &a;
a = -1; //正确,通过变量a本身去修改内容是合法的
*b = -1; //错误,通过指针b去修改指向对象的内容是不合法的
b = &c; //正确,b是指针,可以修改本身的值
另外,由以上例子也可以看出,常量指针(指向常量的指针)也可以指向变量。
指针常量
指针常量本质上是常量,即一个指针类型的常量。声明的时候必须初始化。
声明方式:
T *const A;
const修饰指针A,A不可变,A指向的对象可变。
比如:
int a = 1, int c;
int *const b = &a;
*b = -1; //正确,指针b指向的对象可变,内容可修改
b = &c; //错误,本质上是常量,不可以修改指向的对象
常指针常量
声明方式如下:
const T *const A;
显而易见,指针A和A所指向的对象都不可改变。
const 之 函数 篇
const 修饰 函数参数
const引用,通常应用于定义变量的只读属性的别名,作为函数传入形参,避免在调用函数中意外地被改变。const引用是只读的,常量不能作为左值。
const修饰对象变量(非常量)传入形参( 输出形参不能用const修饰),因为不会产生临时变量,节省了临时对象构造,复制,析构的过程,可提高程序效率。但是对于内部数据类型,没有构造,析构的消耗,复制过程也非常快,因此“值传递”与“引用传递”的效率几乎一样。
const的引用及初始化
对一个常量进行引用,编译器会首先建立一个临时变量,然后将该变量的值置入临时变量中,对该引用的操作就是对该临时变量的操作,对常量的引用可以用其他任何引用来初始化,但是不能改变。
当初始化值是一个左值(可以取得地址)时,没有任何问题;而当初始化值不是一个左值时,则只能对一个常引用赋值,而且赋值的过程是,首先将值隐式转换到类型T,然后将这个转换结果存放在一个临时对象里,最后用这个临时对象初始化该引用变量。
比如:
1). double &dr = 1; //错误,初始化值不是左值
2). const double &cdr = 1; //正确
其中,第二种方法对应的执行过程如下:
double temp = double(1);
const double &cdr = temp;
如果将第一种方法改为合法操作,对应如下:
const int ival = 1024;
const int &refval = ival;
在使用const引用进行函数调用时,即作为函数传入形参,需注意:
#include<iostream>
#include<string>
using namespace std;
void bar(string &s){
cout<<s<<endl;
}
int main(){
bar("hello world"); //非法操作
/* string s = "hello world";
bar(s);*/ //传入的是变量,合法
return 0;
}
原因:
“hello world”串会产生一个临时变量,而在c++中,临时对象都是const类型的,因此该表达式试图将一个const类型的对象转换为非const类型,这是非法的。
应该改为将函数声明改为bar(const string &s);
或者采用值传递bar(stirng s);
const 修饰 函数返回值
const修饰函数返回值,表示返回值(即指针或引用)的内容不能修改。
之所以一定是指针或引用,是因为函数返回值采用“值传递”的方式,则返回值存储于外部临时变量,const修饰没有任何意义。
一般采用引用返回的方式比较少见,常见于类的赋值函数,目的是为了实现链式表达,即A = B = C;
普通情况下,
(A = B) = C;
的表达不常见,但是合法。但是const修饰的函数返回值该操作会报错,因为const常量只能有一次初始化,不能作为左值进行赋值操作。
const 之 类 篇
const 修饰 数据成员
结合关键字static,对数据成员的初始化进行分析,主要有以下几种:
非静态数据成员(const T)
class example{
private:
const int count;
public:
example() = default;
example(const int &count):count(count){}; //只能初始化列表初始化
};
非静态常量数据成员不能在类内初始化,即不能在声明时初始化,也不能在构造函数中初始化,只能在构造函数的初始化列表中初始化。因此,非静态数据成员必须要有构造函数。
1)因为const常量数据成员只能有一次初始化的过程,若在构造函数内部或是在类外,都是对已经完成构造的对象数据成员进行赋值而不是初始化。
2)不同的对象const数据成员的数据可以不同,对象没被创建时,编译器不知道值是什么。
静态非常量数据成员(static T)
class example{
private:
static int count;
};
int example::count = 0; //只能类外初始化,不用再加static关键字
静态非常量数据成员不能在类内初始化,且只能在类外初始化,初始化时不必再加static关键字。
因为static数据成员是所有类对象共享的,在全局数据空间中仅存在一份实例,若是在类内初始化,则每个对象都会包含一个该数据成员,这是矛盾的。
静态常量数据成员(static const T)
class example{
private:
static const int count = 0;
};
//const int count = 0;
静态常量数据成员既可以在类内初始化,也可以在类外初始化
const 修饰 成员函数
const修饰成员函数由两种形式:
- 当const位于函数前,即
const T *Funtional(){};
表示修饰函数返回值(见函数篇); - 当const位于函数后,即
T Funtional() const{};
表示修饰该函数的this指针;
每个const成员函数都有隐含的指针参数
(class)A * const this;
const修饰指针this,表示该成员函数不会修改类的数据,否则编译报错。
const函数可以访问普通成员变量,但是不能修改。除非该数据成员声明有mutable关键字。
const 与 #define(宏定义)
编译器处理
#define在预处理阶段展开(编译阶段替换),无法进行调试;
const常量于编译阶段运行时使用,可用某些集成化的工具调试。
类型
#define是简单的类型替换,没有数据类型,因此替换也没有类型安全检查,可能产生边际效应,出现错误;
const常量则有具体的数据类型,在编译阶段进行类型安全检查。
存储与内存分配
#define在预处理阶段展开,不会分配内存(预处理展开,编译替换才分配内存)
const常量编译阶段使用,需要分配内存。
宏定义存储于程序的代码段,而const常量存储于程序的data段。
#define PI 3.1415 //宏定义常量
const double pi = 3.1415; //const定义,并未将Pi置入只读存储器
double I = PI; //为PI分配内存,仅此一次,以后不再分配
doubel i = pi; //编译替换,每次替换均分配内存
从汇编的角度看,const定义常量只是给出了对应的地址,而不是像#define一样给出的是立即数,所以const常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中由若干拷贝。
const 之 实现机制
const 之 实现机制(借鉴,原理待求证)
const只是编译阶段的保护,编译期间会检查const变量有没有被修改,如果有代码尝试修改,编译报错。const一定程度上在编译阶段使得该变量成为一个常量,但是并没有实现保证该变量在运行阶段内存中的值不被修改。
const关键字是用于约束程序员和编译器为的,而不是程序员用以保证程序正确性的良药,除了全局常量外,没有任何机制保证const声明的常量的不可修改性。