【C++】用11个问题聊聊const八股文

目录

    • 1. 为什么要有const?
    • 2. const常量的用途?
    • 3. const变量的地址是什么样的?
    • 4. const指针
    • 5. const函数
    • 6. const参数
    • 7. const返回值
    • 8. const对象
    • 9. 在const函数中修改成员变量(既要又要)
    • 10. lambda函数与const有关系吗?
    • 11. const对象可以调用非const函数吗?
    • 尾记

1. 为什么要有const?

在很久很久以前,C++的语法就规定了预编译指令#define,用于定义一个宏,并在编译之前,将对应的宏进行文本替换。所以,当我们想定义一个通用的常量,我们可以用宏定义直接进行文本替换。

这样通篇的变换显然很省力呀——就换个文本而已嘛!

但是,这样的力可不兴省呀~

  • 原因一:宏的影响范围是全局的(除非有#undifine),易污染,破坏封装性
    void f(){
    	#define N 123// 宏定义藏在未被调用的函数里
    }
    int main()
    {
        cout << N << endl;//输出123,说明泄露了
        return 0;
    }
    

题外话:宏的另一个缺点,但与const无关
宏定义是在编译之前(也就是预编译阶段)进行字符替换,无法进行类型检查。如果文本替换导致编译出错,编译器才会报错或警告。如果编译没错,但是逻辑错了,因为宏没展开,看起来不直观,debug就会很痛苦,或者程序员们压根就没发现bug。
举个经典八股文例子,#define calculate(x,y) x * y,calculate(1+2, 3+5)。乍一看运算结果应该是3 * 8=24,但正确的结果是1+2*3+5=1+6+5=12。

  • 原因二:宏的每次赋值都会开辟一个新的内存区,但const不会。

    #define N 123的实现原理,仅仅是在预编译时期展开,做文本替换,那么int n = n + N;int n = n + 123;基本没区别。但是每个123的出现,背后都是一次内存分配 + 拷贝——即使它们的值是一样的!
    但是,对于const N = 123;,编译器会直接读取常量区的123,这个过程仅涉及拷贝,不涉及内存分配,所以内存开销更小了。

高能预警,下面我要开始吟唱了!!!

2. const常量的用途?

使用const常量可以让编码更加规范,防止不必要的失误,例如:不小心改值、不小心把值赋给了原本规划为常量的数。这种情景下的const常量一般具有全局性

按照我的个人喜好,我喜欢在局部函数里、获取函数返回值的时候,如果接下来我不会修改这个返回值,那么我会把这个返回值定义为const。其实,在这样”局部“的环境里,const加不加都没关系。但如果加了,别人读我代码的时候会更一目了然一点。

局部const命名用小驼峰,全局用全大写(跟随宏的命名风格)。

const int MAX_NUM = 10;
void f(){
	const float pi = 3.14;
}

3. const变量的地址是什么样的?

const的值存储在常量区。

对比之下,宏只在预编译阶段做文本替换,编译时,其具体的值会作为临时量被开辟在栈区。

【C++】用11个问题聊聊const八股文_第1张图片

这个知识点在编码中,就是,当你的程序需要多次赋值时,用const比直接写值更高效。

4. const指针

  • 常量指针。理解为“常量的指针”即可。这个指针指向的是常量,常量不可改,但指针可改。

    常量↓ 指针↓
    const int* p = &N_CONST;
    
  • 指针常量。理解为“这个指针是个常量”;或者只记上面那个,用排除法。指针不可改,但是指针指向的值可改。

    指针↓ 常量↓
    int* const p = &n;
    
  • 常量的指针常量,debuff拉满

    const int* const p = &N_CONST;
    

5. const函数

  • 【本质】首先要明确的一点是,能被const的一定是类的成员函数。函数本身没有const属性。const 修饰函数, 本质上是修饰了 this 指针

    在Python中,类的成员函数的第一个参数是self,也就是this指针:

    class A:
    def f(self, …):
        pass
    

    相对应地,其实,c++的成员函数的第一个参数也是this指针,只是它藏起来了,编译器帮你自动传入了,所以我们不用显示定义也不用显示地传参。

  • 【意义】在面向对象的编程中,确保成员变量不被污染是一件很重要的事。那么,对于不需要或者不能修改成员变量的函数,我们最好用const给它标记、限制一下,以防在后续维护时,发生无意的、错误的修改。

    例如:

      class A
      {
      public:
          int Get() const { return mValue; }
          int Calculate(int Other) const {
              int Ret = mValue + Other;
              return Ret;
          }
      
      private:
          int mValue;
      };
    
  • 另外,把成员函数修饰为const,方便当对象为const的时候依然能够调用。

6. const参数

  • 对于我们程序员,使用const函数参数可以避免意外地修改指针或地址所指向的值,让代码更规范。
    这样的编程习惯也常见于运算符重载:

    bool operator < (const A& right) const{
      return data < right.data;
    }
    

    可能有萌新会问,如果不想改值,那为什么不直接用类型传递,而是用指针或地址呢?
    因为,对于较为复杂的类型,例如自定义的类、大容器,直接传值会带来不必要的实参拷贝开销,降低性能,而直接传地址能降低开销。

  • 对于编译器,由程序员直接定义的量(一般类型/对象)都是const量

    void f(const int& a){
        cout << "是const" << endl;
    }
    void f(int& c){
        cout << "不是const"<< endl;
    }
    int main()
    {
        f(123);//是const
        return 0;
    }
    

    提问:如果把第一个函数注释掉,程序能运行吗?
    答案是不能。

  • 对于编译器,由程序员创建的对象指针是非const指针,但也可以降级为const指针

      void f2(const A* a){
          cout << "是const指针" << endl;
      }
      void f2(A* a){
          cout << "不是const指针" << endl;
      }
      void f2(const A& a){
          cout << "是const对象" << endl;
      }
      void f2(A& a){
          cout << "不是const对象" << endl;
      }
      int main()
      {
          f2(new A());//不是const指针
          //如果把第2个函数去掉,则输出“是const指针”
      }
    

7. const返回值

常见地,我们会遇到这样的需求: std::string name = item.getName();

使用非const返回值的函数,上述赋值的过程涉及两次拷贝:①item成员变量拷贝到返回值;②返回值拷贝到临时变量name。

对于int、float等字节数较少的类型,这样的拷贝是无所谓的。但对于string、类对象等等大值,这样的拷贝是开销较大的。

更好的做法是返回引用或者指针,省去第①次拷贝

但是引用或者指针作为返回值,是作为左值,是能够被修改的。危!

int& GetAge();// 返回左值,能被修改
const int& GetAgeConst();//返回右值,禁止修改

因此,为了保护指针或引用的内容不被修改,返回值应该设为const。

const std::string& getName() {return mName;}

如果我const的返回值 不是引用 也不是指针呢?像这样:

const std::string getName() {return mName;} 
// 答:一点用都没有!

8. const对象

const对象只能调用const成员函数

问:const对象可以调用static函数吗?
答:伪命题!调用static函数不需要对象。

9. 在const函数中修改成员变量(既要又要)

修改的方法是把变量设为mutable,这是C++11的新特性之一。mutable只能用于类变量,不能与const同时使用。

那么,问题来了,为什么设了const还想着修改呀?!是不是架构没做好!不是,有时候不是:

  • 情景一:函数对外呈现const,对内局部开放mutable。mutable的变量既不用于表示/影响对象状态,也不作为全局管控,只是一个无关的记录量。(网上抄的,我还不太理解)
  • 情景二:受外界制约。外部SDK给的基类函数就是const,我们因为某种需要,继承了这个类,重载了这个const函数,但我们又因为某种特殊需求,我们得在这个const函数里改写什么东西。这时候,这个”东西“就得定义为子类的mutable成员变量。我写Qt的时候遇到过。

10. lambda函数与const有关系吗?

我们不能在lambda函数之内修改值传入的参数。

int a = 5;
auto f = [a]()  { a = 10;}; 
// 报错:表达式必须是可修改的左值

但是我们可以调用值传入参数的非const函数

class A
{
public:
    void f() { cout << "A::f()" << endl; };
};
A a;
auto f = [a]() mutable {
    a.f();
};

如果想修改值传入参数,需要将lambda函数声明为mutable。

注意,lambda函数体内修改值传入参数永远不会影响值本身(这和一般函数是一样的)。除非把参数改为引用传入。

    int a = 5;
    auto f = [a]() mutable {
        a = 10;
        cout << "inner a=" << a << endl; // 10
    };
    f();
    cout << "outer a=" << a << endl; // 5

11. const对象可以调用非const函数吗?

可以,用const_cast把const对象转为非const对象,就可以调用它的非const函数了。

我曾因为受到Qt的制约,做过这种事。

// 函数参数是const对象
void MyDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
// 但我实在是需要调用它的非const函数,也可能是我设计不当
QAbstractItemModel* model = const_cast(index.model());
connect(lockBtn, &QToolButton::clicked, [=] {
    model->setData(index, true, ...);
});
}

尾记

我觉得const的原则是保证值不被意外修改。mutable、const_cast的存在都是为了弥补,补救因外部导致的制约。修改const的行为,可以做,但最好少点做。多次修改const的行为,如果不是外界导致,可能就是设计上存在问题,是不可取的。

你可能感兴趣的:(c++)