深入理解C/C++ 關鍵字const

原文鏈接:https://www.cprogramming.com/tutorial/const_correctness.html

1 常量函數則包含一個this常量指針

這僅僅是編譯器的限制,我們仍然可以繞過編譯器的限制,去改變對象的狀態,可以強制轉換類型

    Fred* pFred = (Fred*)this;

    pFred->intValue = 50;

也可以構造另外一個指向同一塊內存的指針去修改它的狀態。

2 常量對象調用非常量函數,編譯時會產生語法錯誤

 

C++也允許在數據成員的定義前面加上mutable,以允許該成員可以在常量函數中被修改

4 當存在重載函數時,常量對象調用常量成員;非常量對象調用非常量的成員。

int x { 4 }; // initialize x with the value of 4

x = 5; // change value of x to 5

 

However, it’s sometimes useful to define variables with values that can not be changed. For example, consider the gravity of Earth (near the surface): 9.8 meters/second^2. This isn’t likely to change any time soon (and if it does, you’ve likely got bigger problems than learning C++). Defining this value as a constant helps ensure that this value isn’t accidentally changed.

To make a variable constant, simply put the const keyword either before or after the variable type, like so:

 

1

2

const double gravity { 9.8 }; // preferred use of const before type

int const sidesInSquare { 4 }; // okay, but not preferred

 

Although C++ will accept const either before or after the type, we recommend using const before the type because it better follows standard English language convention where modifiers come before the object being modified (e.g. a “green ball”, not a “ball green”).

Const variables must be initialized when you define them, and then that value can not be changed via assignment.

Declaring a variable as const prevents us from inadvertently changing its value:

 

1

2

const double gravity { 9.8 };

gravity = 9.9; // not allowed, this will cause a compile error

 

Defining a const variable without initializing it will also cause a compile error:

 

1

const double gravity; // compiler error, must be initialized upon definition

 

Note that const variables can be initialized from other variables (including non-const ones):

 

1

2

3

4

5

std::cout << "Enter your age: ";

int age;

std::cin >> age;

 

const int usersAge { age }; // usersAge can not be changed

 

Const is often used with function parameters:

 

1

2

3

4

void printInteger(const int myValue)

{

    std::cout << myValue;

}

 

Making a function parameter const does two things. First, it tells the person calling the function that the function will not change the value of myValue. Second, it ensures that the function doesn’t change the value of myValue.

When arguments are passed by value, we generally don’t care if the function changes the value of the parameter (since it’s just a copy that will be destroyed at the end of the function anyway). For this reason, we usually don’t const parameters passed by value. But later on, we’ll talk about other kinds of function parameters (where changing the value of the parameter will change the value of the argument passed in). For these types of parameters, judicious use of const is important.

Runtime vs compile time constants

C++ actually has two different kinds of constants.

Runtime constants are those whose initialization values can only be resolved at runtime (when your program is running). Variables such as usersAge and myValue in the snippets above above are runtime constants, because the compiler can’t determine their initial values at compile time. usersAge relies on user input (which can only be given at runtime) and myValue depends on the value passed into the function (which is only known at runtime). However, once initialized, the value of these constants can’t be changed.

Compile-time constants are those whose initialization values can be resolved at compile-time (when your program is compiling). Variable gravity above is an example of a compile-time constant. Compile-time constants enable the compiler to perform optimizations that aren’t available with runtime constants. For example, whenever gravity is used, the compiler can simply substitute the identifier gravity for the literal double 9.8.

When you declare a const variable, the compiler will implicitly keep track of whether it’s a runtime or compile-time constant.

In most cases, this doesn’t matter, but there are a few odd cases where C++ requires a compile-time constant instead of a run-time constant (such as when defining the length of a fixed-size array -- we’ll cover this later).

constexpr

To help provide more specificity, C++11 introduced new keyword constexpr, which ensures that a constant must be a compile-time constant:

 

1

2

3

4

5

6

7

constexpr double gravity { 9.8 }; // ok, the value of 9.8 can be resolved at compile-time

constexpr int sum { 4 + 5 }; // ok, the value of 4 + 5 can be resolved at compile-time

 

std::cout << "Enter your age: ";

int age;

std::cin >> age;

constexpr int myAge { age }; // not okay, age can not be resolved at compile-time

 

Best practice

 

Any variable that should not be modifiable after initialization and whose initializer is known at compile-time should be declared as constexpr.
Any variable that should not be modifiable after initialization and whose initializer is not known at compile-time should be declared as const.

 

https://www.learncpp.com/cpp-tutorial/const-constexpr-and-symbolic-constants/

///////////////////////////

The const keyword allows you to specify whether or not a variable is modifiable. You can use const to prevent modifications to variables and const pointers and const references prevent changing the data pointed to (or referenced).

But why do you care?

Const gives you the ability to document your program more clearly and actually enforce that documentation. By enforcing your documentation, the const keyword provides guarantees to your users that allow you to make performance optimizations without the threat of damaging their data. For instance, const references allow you to specify that the data referred to won't be changed; this means that you can use const references as a simple and immediate way of improving performance for any function that currently takes objects by value without having to worry that your function might modify the data. Even if it does, the compiler will prevent the code from compiling and alert you to the problem. On the other hand, if you didn't use const references, you'd have no easy way to ensure that your data wasn't modified.

Documentation and Safety

The primary purpose of constness is to provide documentation and prevent programming mistakes. Const allows you to make it clear to yourself and others that something should not be changed. Moreover, it has the added benefit that anything that you declare const will in fact remain const short of the use of forceful methods (which we'll talk about later). It's particularly useful to declare reference parameters to functions as const references:

1

bool verifyObjectCorrectness (const myObj& obj);

Here, a myObj object is passed by reference into verifyObjectCorrectness. For safety's sake, const is used to ensure that verifyObjectCorrectness cannot change the object--after all, it's just supposed to make sure that the object is in a valid state. This can prevent silly programming mistakes that might otherwise result in damaging the object (for instance, by setting a field of the class for testing purposes, which might result in the field's never being reset). Moreover, by declaring the argument const, users of the function can be sure that their object will not be changed and not need to worry about the possible side effects of making the function call.

Syntax Note

When declaring a const variable, it is possible to put const either before or after the type: that is, both

1

int const x = 5;

and

1

const int x = 4;

result in x's being a constant integer. Note that in both cases, the value of the variable is specified in the declaration; there's no way to set it later!

Const Pointers

We've already seen const references demonstrated, and they're pretty natural: when you declare a const reference, you're only making the data referred to const. References, by their very nature, cannot change what they refer to. Pointers, on the other hand, have two ways that you can use them: you can change the data pointed to, or change the pointer itself. Consequently, there are two ways of declaring a const pointer: one that prevents you from changing what is pointed to, and one that prevents you from changing the data pointed to.

The syntax for declaring a pointer to constant data is natural enough:

1

const int *p_int;

You can think of this as reading that *p_int is a "const int". So the pointer may be changeable, but you definitely can't touch what p_int points to. The key here is that the const appears before the *.

On the other hand, if you just want the address stored in the pointer itself to be const, then you have to put const after the *:

1

2

int x;

int * const p_int = &x;

Personally, I find this syntax kind of ugly; but there's not any other obviously better way to do it. The way to think about it is that "* const p_int" is a regular integer, and that the value stored in p_int itself cannot change--so you just can't change the address pointed to. Notice, by the way, that this pointer had to be initialized when it was declared: since the pointer itself is const, we can't change what it points to later on! Them's the rules.

Generally, the first type of pointer, where the data is immutable, is what I'll refer to as a "const pointer" (in part because it's the kind that comes up more often, so we should have a natural way of describing it).

Const Functions

The effects of declaring a variable to be const propagate throughout the program. Once you have a const object, it cannot be assigned to a non-const reference or use functions that are known to be capable of changing the state of the object. This is necessary to enforce the const-ness of the object, but it means you need a way to state that a function should not make changes to an object. In non-object-oriented code, this is as easy as using const references as demonstrated above.

In C++, however, there's the issue of classes with methods. If you have a const object, you don't want to call methods that can change the object, so you need a way of letting the compiler know which methods can be safely called. These methods are called "const functions", and are the only functions that can be called on a const object. Note, by the way, that only member methods make sense as const methods. Remember that in C++, every method of an object receives an implicit this pointer to the object; const methods effectively receive a const this pointer.

The way to declare that a function is safe for const objects is simply to mark it as const; the syntax for const functions is a little bit peculiar because there's only one place where you can really put the const: at the end of the function:

1

2

3

4

<return-value> <class>::<member-function>(<args>) const

{

        // ...

}

For instance,

1

2

3

4

int Loan::calcInterest() const

{

        return loan_value * interest_rate;

}



Note that just because a function is declared const that doesn't prohibit non-const functions from using it; the rule is this:

  • Const functions can always be called
  • Non-const functions can only be called by non-const objects

That makes sense: if you have a const function, all that means is that it guarantees it won't change the object. So just because it is const doesn't mean that non-const objects can't use it.

As a matter of fact, const functions have a slightly stronger restriction than merely that they cannot modify the data. They must make it so that they cannot be used in a way that would allow you to use them to modify const data. This means that when const functions return references or pointers to members of the class, they must also be const.

Const Overloading

In large part because const functions cannot return non-const references to an objects' data, there are many times where it might seem appropriate to have both const and non-const versions of a function. For instance, if you are returning a reference to some member data (usually not a good thing to do, but there are exceptions), then you may want to have a non-const version of the function that returns a non-const reference:

1

2

3

4

int& myClass::getData()

{

        return data;

}

On the other hand, you don't want to prevent someone using a const version of your object,

1

myClass constDataHolder;

from getting the data. You just want to prevent that person from changing it by returning a const reference. But you probably don't want the name of the function to change just because you change whether the object is const or not--among other things, this would mean an awful lot of code might have to change just because you change how you declare a variable--going from a non-const to a const version of a variable would be a real headache.

Fortunately, C++ allows you to overload based on the const-ness of a method. So you can have both const and non-const methods, and the correct version will be chosen. If you wish to return a non-const reference in some cases, you merely need to declare a second, const version of the method that returns a const method:

1

2

3

4

5

// called for const objects only since a non-const version also exists

const int& myData::getData() const

{

        return data;

}

Const iterators

As we've already seen, in order to enforce const-ness, C++ requires that const functions return only const pointers and references. Since iterators can also be used to modify the underlying collection, when an STL collection is declared const, then any iterators used over the collection must be const iterators. They're just like normal iterators, except that they cannot be used to modify the underlying data. (Since iterators are a generalization of the idea of pointers, this makes sense.)

Const iterators in the STL are simple enough: just append "const_" to the type of iterator you desire. For instance, we could iterator over a vector as follows:

1

2

3

4

5

6

7

8

9

10

11

12

std::vector<int> vec;

vec.push_back( 3 );

vec.push_back( 4 );

vec.push_back( 8 );

 

for ( std::vector<int>::const_iterator itr = vec.begin(), end = vec.end();

      itr != end;

      ++itr )

{

        // just print out the values...

        std::cout<< *itr <<std::endl;

}

Note that I used a const iterator to iterate over a non-const collection. Why do that? For the same reason that we normally use const: it prevents the possibility of silly programming mistakes ("oops, I meant to compare the two values, not assign them!") and it documents that we never intend to use the iterator to change the collection.

Const cast

Sometimes, you have a const variable and you really want to pass it into a function that you are certain won't modify it. But that function doesn't declare its argument as const. (This might happen, for instance, if a C library function like strlen were declared without using const.) Fortunately, if you know that you are safe in passing a const variable into a function that doesn't explicitly indicate that it will not change the data, then you can use a const_cast in order to temporarily strip away the const-ness of the object.

Const casts look like regular typecasts in C++, except that they can only be used for casting away constness (or volatile-ness) but not converting between types or casting down a class hierarchy.

1

2

3

4

5

6

7

8

9

10

11

12

// a bad version of strlen that doesn't declare its argument const

int bad_strlen (char *x)

{

        strlen( x );

}

 

// note that the extra const is actually implicit in this declaration since

// string literals are constant

const char *x = "abc";

 

// cast away const-ness for our strlen function

bad_strlen( const_cast<char *>(x) );

Note that you can also use const_cast to go the other way--to add const-ness--if you really wanted to.

Efficiency Gains? A note about Conceptual vs. Bitwise Constness

One common justification for const correctness is based on the misconception that constness can be used as the basis for optimizations. Unfortunately, this is generally not the case--even if a variable is declared const, it will not necessarily remain unchanged. First, it's possible to cast away constness using a const_cast. It might seem like a silly thing to do when you declare a parameter to a function as const, but it's possible. The second issue is that in classes, even const classes can be changed because of the mutable keyword.

Mutable Data in Const Classes

First, why would you ever want to have the ability to change data in a class that's declared const? This gets at the heart of what constness means, and there are two ways of thinking about it. One idea is that of "bitwise constness", which basically means that a const class should have exactly the same representation in memory at all times. Unfortunately (or fortunately), this is not the paradigm used by the C++ standard; instead, C++ uses "conceptual constness". Conceptual constness refers to the idea that the output of the const class should always be the same. This means that the underlying data might change as long as the fundamental behavior remains the same. (In essence, the "concept" is constant, but the representation may vary.)

Why have conceptual constness?

Why would you ever prefer conceptual constness to bitwise constness? One reason is efficiency: for instance, if your class has a function that relies on a value that takes a long time to calculate, it might be more efficient to calculate the value once and then store it for later requests. This won't change the behavior of the function--it will always return the same value. It will, however, change the representation of the class because it must have some place to cache the value.

C++ Support for Conceptual Constness

C++ provides for conceptual constness by using the mutable keyword: when declaring a class, you may specify that some of the fields are mutable:

1

mutable int my_cached_result;

this will allow const functions to change the field regardless of whether or not the object itself was declared as const.

Other Ways of Achieving The Same Gains

If you were planning on using const to increase efficiency, think about what this would really mean--it would be akin to using the original data without making a copy of it. But if you wanted to do that, the simplest approach would just be to use references or pointers (preferably const references or pointers). This gives you a real efficiency gain without relying on compiler optimizations that probably aren't there.

Dangers of Too-much Constness

Beware of exploiting const too much; for instance, just because you can return a const reference doesn't mean that you should return a const reference. The most important example is that if you have local data in a function, you really ought not return a reference to it at all (unless it is static) since it will be a reference to memory that is no longer valid.

Another time when returning a const reference may not a good idea is when you are returning a reference to member data of an object. Although returning a const reference prevents anyone from changing the data by using it, it means that you have to have persistent data to back the reference--it has to actually be a field of the object and not temporary data created in the function. Once you make the reference part of the interface to the class, then, you fix the implementation details. This can be frustrating if you later wish to change your class's private data so the result of the function is computed when the function is invoked rather than actually be stored in the class at all times.

Summary

Don't look at const as a means of gaining efficiency so much as a way to document your code and ensure that some things cannot change. Remember that const-ness propagates throughout your program, so you must use const functions, const references, and const iterators to ensure that it would never be possible to modify data that was declared const.

https://www.cprogramming.com/tutorial/const_correctness.html

////////////////////

https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777416.html

爲什麼使用const?採用符號常量寫出的代碼更容易維護;指針常常是邊讀邊移動,而不是邊寫邊移動;許多函數參數是隻讀不寫的。const最常見用途是作爲數組的界和switch分情況標號(也可以用枚舉符代替),分類如下:

  常變量:  const 類型說明符 變量名

  常引用:  const 類型說明符 &引用名

  常對象:  類名 const 對象名

  常成員函數:  類名::fun(形參) const

  常數組:  類型說明符 const 數組名[大小]    

  常指針:  const 類型說明符* 指針名 ,類型說明符* const 指針名

首先提示的是:在常變量(const 類型說明符 變量名)、常引用(const 類型說明符 &引用名)、常對象(類名 const 對象名)、 常數組(類型說明符 const 數組名[大小]), const” 與 “類型說明符”或“類名”(其實類名是一種自定義的類型說明符) 的位置可以互換。如:

     const int a=5; 與 int const a=5; 等同

     類名 const 對象名 與 const 類名 對象名 等同

 

用法1:常量
    取代了C中的宏定義,聲明時必須進行初始化(!c++類中則不然)。const限制了常量的使用方式,並沒有描述常量應該如何分配。如果編譯器知道了某const的所有使用,它甚至可以不爲該const分配空間。最簡單的常見情況就是常量的值在編譯時已知,而且不需要分配存儲。―《C++ Program Language》
    用const聲明的變量雖然增加了分配空間,但是可以保證類型安全。
    C標準中,const定義的常量是全局的,C++中視聲明位置而定。

用法2:指針和常量
    使用指針時涉及到兩個對象:該指針本身和被它所指的對象。將一個指針的聲明用const“預先固定”將使那個對象而不是使這個指針成爲常量。要將指針本身而不是被指對象聲明爲常量,必須使用聲明運算符*const。
    所以出現在 * 之前的const是作爲基礎類型的一部分:
char *const cp; //到char的const指針
char const *pc1; //到const char的指針
const char *pc2; //到const char的指針(後兩個聲明是等同的)
    從右向左讀的記憶方式:
cp is a const pointer to char. 故pc不能指向別的字符串,但可以修改其指向的字符串的內容
pc2 is a pointer to const char. 故*pc2的內容不可以改變,但pc2可以指向別的字符串

且注意:允許把非 const 對象的地址賦給指向 const 對象的指針,不允許把一個 const 對象的地址賦給一個普通的、非 const 對象的指針。

用法3:const修飾函數傳入參數
    將函數傳入參數聲明爲const,以指明使用這種參數僅僅是爲了效率的原因,而不是想讓調用函數能夠修改對象的值。同理,將指針參數聲明爲const,函數將不修改由這個參數所指的對象。
    通常修飾指針參數和引用參數:
void Fun( const A *in); //修飾指針型傳入參數
void Fun(const A &in); //修飾引用型傳入參數

用法4:修飾函數返回值
    可以阻止用戶修改返回值。返回值也要相應的付給一個常量或常指針。

用法5:const修飾成員函數(c++特性)
const對象只能訪問const成員函數,而非const對象可以訪問任意的成員函數,包括const成員函數;
const對象的成員是不能修改的,而通過指針維護的對象確實可以修改的;
const成員函數不可以修改對象的數據,不管對象是否具有const性質。編譯時以是否修改成員數據爲依據進行檢查。

具體展開來講:
(一). 常量與指針

 常量與指針放在一起很容易讓人迷糊。對於常量指針和指針常量也不是所有的學習C/C++的人都能說清除。例如:

    const int *m1 = new int(10);

    int* const m2 = new int(20);

在上面的兩個表達式中,最容易讓人迷惑的是const到底是修飾指針還是指針指向的內存區域?其實,只要知道:const只對它左邊的東西起作用,唯一的例外就是const本身就是最左邊的修飾符,那麼它纔會對右邊的東西起作用。根據這個規則來判斷,m1應該是常量指針(即,不能通過m1來修改它所指向的內容。);而m2應該是指針常量(即,不能讓m2指向其他的內存模塊)。由此可見:

   1. 對於常量指針,不能通過該指針來改變所指的內容。即,下面的操作是錯誤的:

      int i = 10;

      const int *pi = &i;

      *pi = 100;

      因爲你在試圖通過pi改變它所指向的內容。但是,並不是說該內存塊中的內容不能被修改。我們仍然可以通過其他方式去修改其中的值。例如:

      // 1: 通過i直接修改。

      i = 100;

      // 2: 使用另外一個指針來修改。

      int *p = (int*)pi;

      *p = 100;

      實際上,在將程序載入內存的時候,會有專門的一塊內存區域來存放常量。但是,上面的i本身不是常量,是存放在棧或者堆中的。我們仍然可以修改它的值。而pi不能修改指向的值應該說是編譯器的一個限制。
   2. 根據上面const的規則,const int *m1 = new int(10);我們也可寫作:

      int const *m1 = new int(10);

      這是,理由就不須作過多說明了。
   3. 在函數參數中指針常量時表示不允許將該指針指向其他內容。

      void func_02(int* const p)

      {

      int *pi = new int(100);

      //錯誤!P是指針常量。不能對它賦值。

      p = pi;

      }

      int main()

      {

      int* p = new int(10);

      func_02(p);

      delete p;

      return 0;

      }

   4. 在函數參數中使用常量指針時表示在函數中不能改變指針所指向的內容。

    void func(const int *pi)

    {

    //錯誤!不能通過pi去改變pi所指向的內容!

    *pi = 100;

    }

    int main()

    {

    int* p = new int(10);

    func(p); 

    delete p;

    return 0;

    }

  我們可以使用這樣的方法來防止函數調用者改變參數的值。但是,這樣的限制是有限的,作爲參數調用者,我們也不要試圖去改變參數中的值。因此,下面的操作是在語法上是正確的,但是可能破還參數的值:

    #include <iostream>

    #include <string>

    void func(const int *pi)

    {

    //這裏相當於重新構建了一個指針,指向相同的內存區域。當然就可以通過該指針修改內存中的值了。

    int* pp = (int*)pi;

    *pp = 100;

    }

    int main()

    {

    using namespace std;

    int* p = new int(10);

    cout << "*p = " << *p << endl;

    func(p);

    cout << "*p = " << *p << endl;

    delete p;

    return 0;

    }

(二):常量與引用

    常量與引用的關係稍微簡單一點。因爲引用就是另一個變量的別名,它本身就是一個常量。也就是說不能再讓一個引用成爲另外一個變量的別名, 那麼他們只剩下代表的內存區域是否可變。即:

    int i = 10;

    // 正確:表示不能通過該引用去修改對應的內存的內容。

    const int& ri = i;

    // 錯誤!不能這樣寫。

    int& const rci = i;

    由此可見,如果我們不希望函數的調用者改變參數的值。最可靠的方法應該是使用引用。下面的操作會存在編譯錯誤:

    void func(const int& i)

    {

    // 錯誤!不能通過i去改變它所代表的內存區域。

    i = 100;

    }

    int main()

    {

    int i = 10;

    func(i);

    return 0;

    }

    這裏已經明白了常量與指針以及常量與引用的關係。但是,有必要深入的說明以下。在系統加載程序的時候,系統會將內存分爲4個區域:堆區,棧區,全局區(靜態)和代碼區。從這裏可以看出,對於常量來說,系統沒有劃定專門的區域來保護其中的數據不能被更改。也就是說,使用常量的方式對數據進行保護是通過編譯器作語法限制來實現的。我們仍然可以繞過編譯器的限制去修改被定義爲“常量”的內存區域。看下面的代碼:

    const int i = 10;

    // 這裏i已經被定義爲常量,但是我們仍然可以通過另外的方式去修改它的值。

    // 這說明把i定義爲常量,實際上是防止通過i去修改所代表的內存。

    int *pi = (int*) &i;

局部常量存在哪個段呢? 棧段?

(三):常量函數

    常量函數是C++對常量的一個擴展,它很好的確保了C++中類的封裝性。在C++中,爲了防止類的數據成員被非法訪問,將類的成員函數分成了兩類,一類是常量成員函數(也被稱爲觀察着);另一類是非常量成員函數(也被成爲變異者)。在一個函數的簽名後面加上關鍵字const後該函數就成了常量函數。對於常量函數,最關鍵的不同是編譯器不允許其修改類的數據成員。例如:

    class Test

    {

    public:

    void func() const;

    private:

    int intValue;

    };

    void Test::func() const

    {

    intValue = 100;

    }

    上面的代碼中,常量函數func函數內試圖去改變數據成員intValue的值,因此將在編譯的時候引發異常。

    當然,對於非常量的成員函數,我們可以根據需要讀取或修改數據成員的值。但是,這要依賴調用函數的對象是否是常量。通常,如果我們把一個類定義爲常量,我們的本意是希望他的狀態(數據成員)不會被改變。那麼,如果一個常量的對象調用它的非常量函數會產生什麼後果呢?看下面的代碼:

    class Fred{

    public:

    void inspect() const;

    void mutate();

    };

    void UserCode(Fred& changeable, const Fred& unChangeable)

    {

    changeable.inspect(); // 正確,非常量對象可以調用常量函數。

    changeable.mutate(); // 正確,非常量對象也允許修改調用非常量成員函數修改數據成員。

    unChangeable.inspect(); // 正確,常量對象只能調用常理函數。因爲不希望修改對象狀態。

    unChangeable.mutate(); // 錯誤!常量對象的狀態不能被修改,而非常量函數存在修改對象狀態的可能

    }

    從上面的代碼可以看出,由於常量對象的狀態不允許被修改,因此,通過常量對象調用非常量函數時將會產生語法錯誤。實際上,我們知道每個成員函數都有一個隱含的指向對象本身的this指針。而常量函數則包含一個this常量指針。如下:

    void inspect(const Fred* this) const;

    void mutate(Fred* this);

     也就是說對於常量函數,我們不能通過this指針去修改對象對應的內存塊。但是,在上面我們已經知道,這僅僅是編譯器的限制,我們仍然可以繞過編譯器的限制,去改變對象的狀態。看下面的代碼:

    class Fred{

    public:

    void inspect() const;
    private:

    int intValue;

    };

    void Fred::inspect() const

    {

    cout << "At the beginning. intValue = "<< intValue << endl;

    // 這裏,我們根據this指針重新定義了一個指向同一塊內存地址的指針。

    // 通過這個新定義的指針,我們仍然可以修改對象的狀態。

    Fred* pFred = (Fred*)this;

    pFred->intValue = 50;

    cout << "Fred::inspect() called. intValue = "<< intValue << endl;

    }

    int main()

    {

    Fred fred;

    fred.inspect();

    return 0;

    }

    上面的代碼說明,只要我們願意,我們還是可以通過常量函數修改對象的狀態。同理,對於常量對象,我們也可以構造另外一個指向同一塊內存的指針去修改它的狀態。這裏就不作過多描述了。

    另外,也有這樣的情況,雖然我們可以繞過編譯器的錯誤去修改類的數據成員。但是C++也允許我們在數據成員的定義前面加上mutable,以允許該成員可以在常量函數中被修改。例如:

    class Fred{

    public:

    void inspect() const;

    private:

    mutable int intValue;

    };

    void Fred::inspect() const

    {

    intValue = 100;

    }

    但是,並不是所有的編譯器都支持mutable關鍵字。這個時候我們上面的歪門邪道就有用了。

    關於常量函數,還有一個問題是重載。

    #include <iostream>

    #include <string>

    using namespace std;

    class Fred{

    public:

    void func() const;

    void func();

    };

    void Fred::func() const

    {

    cout << "const function is called."<< endl;

    }

    void Fred::func()

    {

    cout << "non-const function is called."<< endl;

    }

    void UserCode(Fred& fred, const Fred& cFred)

    {

    cout << "fred is non-const object, and the result of fred.func() is:" << endl;

    fred.func();

    cout << "cFred is const object, and the result of cFred.func() is:" << endl;

    cFred.func();

    }

    int main()

    {

    Fred fred;

    UserCode(fred, fred);

    return 0;

    }

    輸出結果爲:

    fred is non-const object, and the result of fred.func() is:

    non-const function is called.

    cFred is const object, and the result of cFred.func() is:

    const function is called.

    從上面的輸出結果,我們可以看出。當存在同名同參數和返回值的常量函數和非常量函數時,具體調用哪個函數是根據調用對象是常量對像還是非常量對象來決定的。常量對象調用常量成員;非常量對象調用非常量的成員。

    總之,我們需要明白常量函數是爲了最大程度的保證對象的安全。通過使用常量函數,我們可以只允許必要的操作去改變對象的狀態,從而防止誤操作對對象狀態的破壞。但是,就像上面看見的一樣,這樣的保護其實是有限的。關鍵還是在於我們開發人員要嚴格的遵守使用規則。另外需要注意的是常量對象不允許調用非常量的函數。這樣的規定雖然很武斷,但如果我們都根據原則去編寫或使用類的話這樣的規定也就完全可以理解了。
(四):常量返回值

     很多時候,我們的函數中會返回一個地址或者引用。調用這得到這個返回的地址或者引用後就可以修改所指向或者代表的對象。這個時候如果我們不希望這個函數的調用這修改這個返回的內容,就應該返回一個常量。這應該很好理解,大家可以去試試。

+++++++++++++++++++++++++++++++++++++++

c++ 中const 

+++++++++++++++++++++++++++++++++++++++

1. const常量,如const int max = 100;  
優點:const常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查,而對後者只進行字符替換,沒有類型安全檢查,並且在字符替換時可能會產生意料不到的錯誤(邊際效應)
2.  const 修飾類的數據成員。如:
class A
{

    const int size;

    …

}

const數據成員只在某個對象生存期內是常量,而對於整個類而言卻是可變的。因爲類可以創建多個對象,不同的對象其const數據成員的值可以不同。所以不能在類聲明中初始化const數據成員,因爲類的對象未被創建時,編譯器不知道const 數據成員的值是什麼。如

class A

{

 const int size = 100;    //錯誤

 int array[size];         //錯誤,未知的size

}
const數據成員的初始化只能在類的構造函數的初始化表中進行。要想建立在整個類中都恆定的常量,應該用類中的枚舉常量來實現。如

class A

{…

 enum {size1=100, size2 = 200 };

int array1[size1];

int array2[size2];

}

枚舉常量不會佔用對象的存儲空間,他們在編譯時被全部求值。但是枚舉常量的隱含數據類型是整數,其最大值有限,且不能表示浮點數。

3. const修飾指針的情況,見下式:

int b = 500; 
const int* a = &                  [1] 
int const *a = &                  [2] 
int* const a = &                  [3] 
const int* const a = &       [4]

如果你能區分出上述四種情況,那麼,恭喜你,你已經邁出了可喜的一步。不知道,也沒關係,我們可以參考《Effectivec++》Item21上的做法,如果const位於星號的左側,則const就是用來修飾指針所指向的變量,即指針指向爲常量;如果const位於星號的右側,const就是修飾指針本身,即指針本身是常量。因此,[1]和[2]的情況相同,都是指針所指向的內容爲常量(const放在變量聲明符的位置無關),這種情況下不允許對內容進行更改操作,如不能*a = 3;[3]爲指針本身是常量,而指針所指向的內容不是常量,這種情況下不能對指針本身進行更改操作,如a++是錯誤的;[4]爲指針本身和指向的內容均爲常量。

4. const的初始化

先看一下const變量初始化的情況 
1) 非指針const常量初始化的情況:A b; 
const A a = b;

2) 指針const常量初始化的情況:

A* d = new A(); 
const A* c = d; 
或者:const A* c = new A(); 
3)引用const常量初始化的情況: 
A f; 
const A& e = f;      // 這樣作e只能訪問聲明爲const的函數,而不能訪問一般的成員函數;

    [思考1]: 以下的這種賦值方法正確嗎? 
    const A* c=new A(); 
    A* e = c; 
    [思考2]: 以下的這種賦值方法正確嗎? 
    A* const c = new A(); 
    A* b = c;

5. 另外const 的一些強大的功能在於它在函數聲明中的應用。在一個函數聲明中,const可以修飾函數的返回值,或某個參數;對於成員函數,還可以修飾是整個函數。有如下幾種情況,以下會逐漸的說明用法:A&operator=(const A& a); 
void fun0(const A* a ); 
void fun1( ) const; // fun1( ) 爲類成員函數 
const A fun2( );

1) 修飾參數的const,如 void fun0(const A* a ); void fun1(const A& a); 
調用函數的時候,用相應的變量初始化const常量,則在函數體中,按照const所修飾的部分進行常量化,如形參爲const A*a,則不能對傳遞進來的指針的內容進行改變,保護了原指針所指向的內容;如形參爲const A&a,則不能對傳遞進來的引用對象進行改變,保護了原對象的屬性。 
[注意]:參數const通常用於參數爲指針或引用的情況,且只能修飾輸入參數;若輸入參數採用“值傳遞”方式,由於函數將自動產生臨時變量用於複製該參數,該參數本就不需要保護,所以不用const修飾。

[總結]對於非內部數據類型的輸入參數,因該將“值傳遞”的方式改爲“const引用傳遞”,目的是爲了提高效率。例如,將void Func(A a)改爲void Func(const A &a)

      對於內部數據類型的輸入參數,不要將“值傳遞”的方式改爲“const引用傳遞”。否則既達不到提高效率的目的,又降低了函數的可理解性。例如void Func(int x)不應該改爲void Func(const int &x)

2)  修飾返回值的const,如const A fun2( ); const A* fun3( ); 
這樣聲明瞭返回值後,const按照"修飾原則"進行修飾,起到相應的保護作用。const Rational operator*(const Rational& lhs, const Rational& rhs) 

return Rational(lhs.numerator() * rhs.numerator(), 
lhs.denominator() * rhs.denominator()); 
}

返回值用const修飾可以防止允許這樣的操作發生:Rational a,b; 
Radional c; 
(a*b) = c;

一般用const修飾返回值爲對象本身(非引用和指針)的情況多用於二目操作符重載函數併產生新對象的時候。 
[總結]

1.  一般情況下,函數的返回值爲某個對象時,如果將其聲明爲const時,多用於操作符的重載。通常,不建議用const修飾函數的返回值類型爲某個對象或對某個對象引用的情況。原因如下:如果返回值爲某個對象爲const(const A test = A實例)或某個對象的引用爲const(const A& test = A實例),則返回值具有const屬性,則返回實例只能訪問類A中的公有(保護)數據成員和const成員函數,並且允許對其進行賦值操作,這在一般情況下很少用到。

2.  如果給採用“指針傳遞”方式的函數返回值加const修飾,那麼函數返回值(即指針)的內容不能被修改,該返回值只能被賦給加const 修飾的同類型指針。如:

const char * GetString(void);

如下語句將出現編譯錯誤:

char *str=GetString();

正確的用法是:

const char *str=GetString();

3.     函數返回值採用“引用傳遞”的場合不多,這種方式一般只出現在類的賻值函數中,目的是爲了實現鏈式表達。如:

class A

{…

 A &operate = (const A &other);  //負值函數

}
A a,b,c;              //a,b,c爲A的對象

a=b=c;            //正常

(a=b)=c;          //不正常,但是合法

若負值函數的返回值加const修飾,那麼該返回值的內容不允許修改,上例中a=b=c依然正確。(a=b)=c就不正確了。
[思考3]: 這樣定義賦值操作符重載函數可以嗎? 
const A& operator=(const A& a);

6.     類成員函數中const的使用 
一般放在函數體後,形如:void fun() const; 
任何不會修改數據成員的函數都因該聲明爲const類型。如果在編寫const成員函數時,不慎修改了數據成員,或者調用了其他非const成員函數,編譯器將報錯,這大大提高了程序的健壯性。如:

class Stack

{

 public:

      void Push(int elem);

      int Pop(void);

      int GetCount(void) const;   //const 成員函數

 private:

      int m_num;

      int m_data[100];

};

int Stack::GetCount(void) const

{

  ++m_num;              //編譯錯誤,企圖修改數據成員m_num

  Pop();                    //編譯錯誤,企圖調用非const函數

  Return m_num;

}

7. 使用const的一些建議

1) 要大膽的使用const,這將給你帶來無盡的益處,但前提是你必須搞清楚原委; 
2) 要避免最一般的賦值操作錯誤,如將const變量賦值,具體可見思考題; 
3) 在參數中使用const應該使用引用或指針,而不是一般的對象實例,原因同上; 
4) const在成員函數中的三種用法(參數、返回值、函數)要很好的使用; 
5) 不要輕易的將函數的返回值類型定爲const; 
6) 除了重載操作符外一般不要將返回值類型定爲對某個對象的const引用;

[思考題答案] 
1) 這種方法不正確,因爲聲明指針的目的是爲了對其指向的內容進行改變,而聲明的指針e指向的是一個常量,所以不正確; 
2) 這種方法正確,因爲聲明指針所指向的內容可變; 
3) 這種做法不正確; 
在const A::operator=(const A& a)中,參數列表中的const的用法正確,而當這樣連續賦值的時侯,問題就出現了: 
A a,b,c: 
(a=b)=c; 
因爲a.operator=(b)的返回值是對a的const引用,不能再將c賦值給const常量。

++++++++++++++++++++++++++++++++++++++++

const 在c和c++中的區別  

http://tech.e800.com.cn/articles/2009/722/1248229886744_1.html

++++++++++++++++++++++++++++++++++++++++

1. C++中的const正常情況下是看成編譯期的常量,編譯器並不爲const分配空間,只是在編譯的時候將期值保存在名字表中,並在適當的時候摺合在代碼中.所以,以下代碼:
using namespace std;
int main()
{
const int a = 1;
const int b = 2;
int array[ a + b ] = {0};
for (int i = 0; i < sizeof array / sizeof *array; i++)
{
cout << array << endl;
}
}
在可以通過編譯,並且正常運行.但稍加修改後,放在C編譯器中,便會出現錯誤:
int main()
{
int i;
const int a = 1;
const int b = 2;
int array[ a + b ] = {0};
for (i = 0; i < sizeof array / sizeof *array; i++)
{
printf("%d",array);
}
}
錯誤消息:
c:\test1\te.c(8): error C2057: 應輸入常數表達式
c:\test1\te.c(8): error C2466: 不能分配常數大小爲 0 的數組
出現這種情況的原因是:在C中,const是一個不能被改變的普通變量,既然是變量,就要佔用存儲空間,所以編譯器不知道編譯時的值.而且,數組定義時的下標必須爲常量.
2. 在C語言中: const int size; 這個語句是正確的,因爲它被C編譯器看作一個聲明,指明在別的地方分配存儲空間.但在C++中這樣寫是正確的.C++中const默認是內部連接,如果想在C++中達到以上的效果,必須要用extern關鍵字.即C++中,const默認使用內部連接.而C中使用外部連接.
(1) 內連接:編譯器只對正被編譯的文件創建存儲空間,別的文件可以使用相同的表示符或全局變量.C/C++中內連接使用static關鍵字指定.
(2) 外連接:所有被編譯過的文件創建一片單獨存儲空間.一旦空間被創建,連接器必須解決對這片存儲空間的引用.全局變量和函數使用外部連接.通過extern關鍵字聲明,可以從其他文件訪問相應的變量和函數.
/* C++代碼  header.h */
const int test = 1;
/* C++代碼  test1.cpp */
#include "header.h"
using namespace std;
int main() { cout << "in test1 :" << test << endl; }
/* C++代碼 test2.cpp */
#include "header.h"
using namespace std;
void print() { cout << "in test2:" << test << endl;}
以上代碼編譯連接完全不會出問題,但如果把header.h改爲:
extern const int test = 1;
在連接的時候,便會出現以下錯誤信息:
test2 error LNK2005: "int const test" (?test@@3HB) 已經在 test1.obj 中定義
    因爲extern關鍵字告訴C++編譯器test會在其他地方引用,所以,C++編譯器就會爲test創建存儲空間,不再是簡單的存儲在名字表裏面.所以,當兩個文件同時包含header.h的時候,會發生名字上的衝突.
此種情況和C中const含義相似:
/* C代碼 header.h */
const int test = 1;
/* C代碼 test1.c */ 
#include "header.h"
int main() { printf("in test1:%d\n",test); }
/* C代碼 test2.c */
#include "header.h"
void print() { printf("in test2:%d\n",test); }
錯誤消息:
test3 fatal error LNK1169: 找到一個或多個多重定義的符號
test3 error LNK2005: _test 已經在 test1.obj 中定義

也就是說:在c++ 中const 對象默認爲文件的局部變量。與其他變量不同,除非特別說明,在全局作用域聲明的 const 變量是定義該對象的文件的局部變量。此變量只存在於那個文件中,不能被其他文件訪問。通過指定 const 變更爲 extern,就可以在整個程序中訪問 const 對象:
      // file_1.cc
      // defines and initializes a const that is accessible to other files
      extern const int bufSize = fcn();
      // file_2.cc
      extern const int bufSize; // uses bufSize from file_1
      // uses bufSize defined in file_1
      for (int index = 0; index != bufSize; ++index)
            // ...

3. C++中,是否爲const分配空間要看具體情況.如果加上關鍵字extern或者取const變量地址,則編譯器就要爲const分配存儲空間.
4. C++中定義常量的時候不再採用define,因爲define只做簡單的宏替換,並不提供類型檢查.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章