68.C++中的const

来源:https://www.cnblogs.com/codemagiciant/archive/2023/03/16/17224453.html
-Advertisement-
Play Games

編寫程式過程中,我們有時不希望改變某個變數的值。此時就可以使用關鍵字 const 對變數的類型加以限定。 初始化和const 因為const對象一旦創建後其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式: const int i = get_size();//正 ...


  編寫程式過程中,我們有時不希望改變某個變數的值。此時就可以使用關鍵字 const 對變數的類型加以限定。

初始化和const

  因為const對象一旦創建後其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式:

const int i = get_size();//正確:運行時初始化
const int j = 42;//正確:編譯時初始化
const int k;//錯誤:k是一個未經初始化的常量 

  正如之前反覆提到的,對象的類型決定了其上的操作。與非const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執行不改變其內容的操作。例如,const int和普通的int一樣都能參與算術運算,也都能轉換成一個布爾值,等等 。
在不改變const對象的操作中還有一種是初始化,如果利用一個對象去初始化另外 一個對象,則它們是不是const都無關緊要:

int i = 42;
const int ci = i;//正確: 1的值被拷貝給了ci
int j = ci;//正確:ci的值被拷貝給了J

預設狀態下,const對象僅在文件內有效

  當以編譯時 初始化的方式定義 一個const對象時,就如對bufSize的定義 一樣:

const int bufSize = 512;//輸入緩衝區大小

  編譯器將在編譯過程中把用到該變數的地方都替換成對應的值。也就是說,編譯器會找到代碼中所有用到 bufSize的地方,然後用512替換。為了執行上述替換,編譯器必須知道變數的初始值 。如果程式包含多個文件,則每個 用了const對象的 文件都必須得能訪問到它的初始值才行。要做到這一點,就必須在每 一個用到變數的文件中都有對它的定義(參見C++Primer2.2.2節,第41頁)。為了支持這一用法, 同時避免對同一變數的重覆定義,預設情況下,const對象被設定為僅在文件內有效。當多個文件中出現了同名的const變數時,其實等同於在不同文件中分別定義了獨立的變數。
  某些時候有這樣一種const變數,它的初始值不是一個常旦表達式,但又確實有必要在文件間共用。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變數。相反,我們想讓這類const對象像其他(非常量)對象一樣工作,也就是說,只在一個文件中 定義const,而在其他多個文件中聲明並使用它。
  解決的辦法是,對於const變數不管是聲明還是定義都添加extern關鍵字,這樣只需定義一次就可以了:

//file_l.cc定義並初始化了一個常岳,該常量能被其他文件訪問
extern const int bufSize =fen();
//file_l.h頭文件
extern const int bufSize;//與file_l.cc中定義的bufSize是同一個

1.變數中的const

1.1 普通變數

直接在普通變數類型聲明符前加上 const,可以將其聲明為 const 類型:

const int a = 0;

這樣就把 a 聲明成了一個 const 類型的常量,所以我們不能再改變它的值了,所以下麵試圖改變 a 的語句將會編譯報錯:

a = 10;

修改局部變數的值:

1.如果const修飾的局部變數是基礎的類型(int char double等等),並且初始化使用字面常量的話,不會給該變數分配空間。
例如:

void test()
{
	const int a = 10;//用字面常量10來初始化
	a = 20;//error
}

2.但是,當我們對這個變數進行取地址的操作的時候,系統會為該變數分配空間。

void test() 
{
	const int a = 10;
	//a = 20;//error
	int* p = (int*)&a;
	*p = 20;
	cout << a << endl;
	cout << *p << endl;
}

上面的結果是:10和20

  這是因為,當我們定義一個被const修飾並且使用字面常量來初始化的局部變數的時候,系統會把這個變數看作是一個符號,放入到符號表中,這麼變數名就是一個符號,值就是這個符號的值,類似於#define的作用。(這就是 C++ 中的常量摺疊 ,因為常量是在運行時初始化的,編譯器對常量進行優化,直接將常量值放在編譯器的符號表中,使用常量時直接從符號表中取出常量的值,省去了訪存這一步驟。)

  當我們對這個變數取地址的時候,由於原來沒有空間,就沒有地址,現在需要取地址,所以才被迫分配一塊空間,我們通過地址的解引用可以修改這個空間的值,這也就是為什麼第二個結果為20的原因,但是如果我們還是通過變數名來訪問數據的話,系統會認為這還是一個符號,直接用符號表裡面的值替換。

但是!

3.如果初始化不是用字面常量而是用變數,那麼系統會直接分配空間。

void test() 
{
	int b = 20;
	const int a = b;
}

這時候的a是有空間的,不會被放入到符號表中。

修改全局變數的值

  通過指針修改位於靜態存儲區的的const變數,語法上沒有報錯,編譯不會出錯,一旦運行就會報告異常。因為全局變數存儲於靜態存儲區,靜態存儲區中的常量只有讀許可權,不能修改它的值。

  與C一樣,當const修飾普通的全局變數的時候,不能通過變數名和地址來修改變數的值。

另外

  與C不一樣的是,C語言中的const修飾的普通全局變數預設是外部鏈接屬性的,但是在C++中被const修飾的普通全局變數是內部鏈接屬性的。

  也就是說當我們在一個文件中定義了一個如下的全局變數

const int a = 10;//定義全局變數

int main() 
{
	return 0;
}

  我們在另外一個文件中,使用extern來聲明,也是不可以的。

//另外一個文件

extern const int a;//在另外的文件中聲明

  上面這種做法是不可以的,C++中被const修飾的全局變數預設是內部鏈接屬性,不能直接在另外的文件中使用,如果想要在另外的文件中使用,就需要在定義該全局的變數的文件中用extern來修飾(另一個文件也需要extern修飾)。

//定義的文件
extern const int a = 10;
//另外一個文件聲明
extern const int a;

原文鏈接:https://blog.csdn.net/weixin_61021362/article/details/121544469

1.2 const 修飾引用

  我們還可以對引用使用 const 限定符,在引用聲明的類型聲明符前加上 const 就可以聲明對const的引用,常量引用不能用來修改它所綁定的對象。

引用綁定到同一種類型,並修改值

直接上例子:

int i = 0;
const int j = 0;
const int &r1 = i;
//r1 = 20;//err不能給常量賦值	
const int &r2 = j;
//r2 = 20;//err不能給常量賦值	
int &r3 = j;

  第三行將非常量對象 i 綁定到 const 引用 r1 上,此過程中發生了隱式類型轉換,i 的類型為 int,r1 的類型為 const int &, 所以這個過程 i 就從 int 轉換為了 const int,所以不能通過 r1 改變 i 的值,但可以直接改變 i 的值。但是 const int 類型不能轉換為 int。

可以這樣理解:const int是int的一種,但是範圍更小,將int限定在一個範圍之類,(本身int = const int類型 + 非const類型),沒有問題。但是const int到int範圍擴大,超出許可權。

  第五行將常量對象 j 綁定到 const 引用 r2 上,不能直接改變 j 的值也不能通過常量引用改變 j 的值。
  第七行將常量對象綁定到 const 引用 r3 上,報錯,不能將常量對象綁定到常量引用上。

綁定到另一種類型,並修改值

直接上例子:

double i= 1.0;
const int &r1 = i; 
i = 2.0;
cout << "i = " << i << endl;
cout << "r1 = " << r1 <<endl;
---------------------------------------
out:
i = 2;
r1 = 1;

  上面的代碼將 int 型的引用 r1 綁定到 double 型變數 i 上,然後改變 i 的值,我們發現 r1 並沒有改變,它的值反而是綁定 i 時 i 的值。這是因為引用變數的類型與被引用對象的類型不同時,中間會有如下操作:

double i = 1.0;
int temp = i;
const int &r1 = temp;

  r1 引用的是臨時量 temp,而不是 i,所以才會出現上面的情況。

1.3 const 修飾指針

  當使用const修飾指針變數時,情況就複雜起來了。const可以放置在不同的地方,因此具有不同的含義。來看下麵一個例子:

int age = 39;
const int * p1 = &age;
int const * p2 = &age;
int * const p3 = &age;
const int * const p4 = &age;

  二三行是一個意思,表示 p 是指向常量的指針;第四行表示 p 是常量指針;第五行表示 p 是指向常量的常量指針。
  上面二三行的賦值同樣發生了類型轉換,從 int * 轉換為 const int *。

指向常量的指針和常量指針

顧名思義:常量指針就是指針本身是常量,指針的值不能改變,也就是指針不能改變指向的對象,所以常量指針必須初始化;

指向常量的指針就是指向的變數時常量,被指變數不能被修改。

也可以將兩者結合,就有了指向常量的常量指針,其具有指向常量的指針和常量指針的共同性質。

修改指向常量的指針和常量指針

int age2 = 20;
*p1 = 20;
*p3 = 20;
p1 = &age2;
p3 = &age2;

  第二行會報錯,因為 p1 是指向常量的指針,不能通過指針修改 age 的值;第五行會報錯,因為 p3 是常量指針,只能指向 age,不能指向其他變數。

  如果對age2進行修改是不會報錯的。
原文鏈接:https://blog.csdn.net/weixin_45773137/article/details/126297568

1.4頂層與底層const

  任意常量對象為頂層const,包括常量指針;指向常量的指針和聲明const的引用都為底層const

  頂層const(top-level const)表示指針本身是個常量 int* const ptr=&m;

  此時指針不可以發生改變,但是指針所指向的對象值是可以改變的

  底層const(low-level const)表示指針所指的對象是常量 const int* ptr=&m;

  此時指針可以發生改變,但是指針所指向的對象值是不可以改變的

  頂層const可以表示任意的對象是常量(指針、引用、int、double都可以)

  於是只有指針和引用等複合類型可以是底層const

  執行對象的拷貝構造時,常量是頂層const還是底層const差別明顯

  頂層const並不會有任何影響

進行拷貝操作的時候,僅僅只是從右值(頂層const)拷貝一個值並給自己賦值,雖然右值是一個不可變的量,但是貌似對我自己的拷貝完全沒有影響吧

const int m = 10;
int n = m;
int* const ptr2 = &n;
int* ptr3 = ptr2;
int i= 0; 
int *const p1 = &i;//不能改變p1的值,這是一個頂層const
const int ci = 42;//不能改變ci的值,這是一個頂層const
const int *p2 = &ci;//允許改變p2的值 這是一個底層const
const int *const p3 = p2;//靠右的const是頂層const, 靠左的是底層
const const int &r = ci;//用於聲明引用的const都是底層const

  當執行對象的拷貝操作時, 常量是頂層const還是底層const區別明顯。 其中,頂層const不受什麼影響:

i = ci;//正確:拷貝ci的值,CI是 一個頂層const, 對此操作無影響
p2 = p3;//正確:p2和p3指向的對象類型相同,p3頂層const的部分不影響

  執行拷貝操作並不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什麼 影響。

  另一方面,底層const的限制卻不能忽視。 當執行對象的拷貝操作時拷入和拷出的對象必須具有相同的底層const資格, 或者兩個對象的數據類型必須能夠轉換。非常量可以轉換成常扯, 反之則不行:

int *p = p3;//錯誤:p3包含底層const的定義,而p沒有
p2 = p3;//正確:p2和p3都是底層const
p2 = &i;//正確:int*能轉換成const int* 
int &r = ci;//錯誤:普通的int&不能綁定到int常量上
const int &r2 = 1;//正確:const int&可以綁定到一個普通int上

  p3既是頂層const也是底層const,拷貝p3時可以不在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量。因此,不能用p3去初始化p, 因為p指向的是一 個普通的(非常量)整數。 另一方面,p3的值可以賦給p2,是因為這兩個指針都是底層 const,儘管p3同時也是一個常量指針(頂層const), 僅就這次賦值而言不會有什麼影響。

原文鏈接:https://blog.csdn.net/m0_64860543/article/details/128269607

2.const 函數形參

  我們已經瞭解了變數中const修飾符的作用,調用函數就會涉及變數參數的問題,那麼在形參列表中const形參與非const形參有什麼區別呢?

2.1 const 修飾普通形參

同樣,先來看看普通變數:

void fun(const int i)
{
	i = 0;
    cout << i << endl;
}

void fun(int i)
{
	i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    fun(i);
    return 0;
}

  形參的頂層 const 在初始化時會被忽略,所以上面定義的兩個函數實際上是一個函數。編譯時會出現void fun(int) previously defined here錯誤。

  • 由於普通變數是拷貝傳值,所以const int實參可以傳給 int 形參。

  • 與普通 const 變數一樣,第一個 fun 中的形參 i 只可讀;第二個function中的 i 則可讀可寫。

2.2 const 修飾指針形參

  與 const 指針變數一樣,指向常量的指針形參指向的值不能修改;常量指針形參不能指向其他變數;指向常量的常量指針形參指向的值不能被修改,也不能指向其他變數。

#include<iostream>
using namespace std;
void fun(const int* i)
{
    cout << *i << endl;
}

void fun(int* i)
{
    *i = 0;
    cout << *i << endl;
}

int main()
{
    const int i = 1;
    //調用 fun(const int* i),沒有 fun(const int* i),則會編譯報錯,因為沒有匹配形參的函數。
    fun(&i);  
    int j = 1;
    //調用 fun(int* i),沒有 fun(int* i),則會調用 fun(const int* i),此時 j 的值不會被改變
    fun(&j);  
    return 0;
}

  p1 指向的值不能修改;p2 不能指向其他變數;p3 指向的值不能被修改,也不能指向其他變數。

此外,形參的底層 const 在初始化時不會被忽略,所以上面的兩個函數是不同的函數,即重載函數,上面例子編譯並不會報錯,若果再加上一個void fun(int *const i)就會報錯,因為這個函數定義裡面 i 是頂層 const。

2.3 const 修飾引用形參

  與 const 引用一樣,const 引用不會改變被引用變數的值。

#include<iostream>
using namespace std;
void fun(const int& i)
{
    cout << i << endl;
}

void fun(int& i)
{
    i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    //調用 fun(const int& i),沒有 fun(const int& i),則會編譯報錯,因為沒有匹配形參的函數。
    fun(i);
    int j = 1;
    //調用 fun(int& i),沒有 fun(int& i),則會調用 fun(const int& i),此時 j 的值不會被改變
    fun(j);
    return 0;
}

由於 const 引用也是底層 const ,所以上面兩個函數是不同的函數,即重載函數,編譯並不會報錯。

3.類常量成員函數

  面向對象程式設計中,為了體現封裝性,通常不允許直接修改類對象的數據成員。若要修改類對象,應調用公有成員函數來完成。為了保證const對象的常量性,編譯器須區分試圖修改類對象與不修改類對象的函數。例如:

const Screen blankScreen;
blankScreen.display();   // 對象的讀操作
blankScreen.set(‘*’);    // 錯誤:const類對象不允許修改

  C++中的常量對象,以及常量對象的指針或引用都只能調用常量成員函數。

  要聲明一個const類型的類成員函數,只需要在成員函數參數列表後加上關鍵字const,例如:

class Screen 
{
public:
   char get() const;
};

在類外定義const成員函數時,還必須加上const關鍵字:

char Screen::get() const 
{
   return screen[cursor];
}

若將成員成員函數聲明為const,則該函數不允許修改類的數據成員。例如:

class Screen 
{
public:
    int get_cursor() const {return cursor; }
    int set_cursor(int intival) const { cursor = intival; }
};

在上面成員函數的定義中,get_cursor()的定義是合法的,set_cursor()的定義則非法。

值得註意的是,把一個成員函數聲明為const可以保證這個成員函數不修改數據成員,但是,如果據成員是指針,則const成員函數並不能保證不修改指針指向的對象,編譯器不會把這種修改檢測為錯誤。例如:

class Name
{
public:
    void setName(const string &s) const;
    char *getName() const;
private:
    char *m_sName;
};
 
void setName(const string &s) const 
{
    m_sName = s.c_str();      // 錯誤!不能修改m_sName;
 
    for (int i = 0; i < s.size(); ++i) 
        m_sName[i] = s[i];    // 不是錯誤的
}

const成員函數可以被具有相同參數列表的非const成員函數重載,例如:

class Screen 
{
public:
    char get(int x,int y);
    char get(int x,int y) const;
};

在這種情況下,類對象的常量性決定調用哪個函數。

const Screen cs;
Screen cc2;
char ch = cs.get(0, 0);  // 調用const成員函數
ch = cs2.get(0, 0);     // 調用非const成員函數

const成員函數不能修改類對象數據成員的深層解析:

調用成員函數時,通過一個名為this的隱式參數來訪問調用該函數的對象成員。例如:

Name bozai;
bozai.setName("bozai");
bozai.getName("BOZAI");

原文鏈接:https://blog.csdn.net/weixin_45773137/article/details/126297568


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 迭代器模式(Iterator Pattern):提供一種方法順序訪問一個聚合對象中的各個元素,而不需要暴露該對象的內部表示。在JavaScript中,可以使用迭代器模式來操作數組或類數組對象。 在迭代器模式中,集合對象包含一個方法,用於返回一個迭代器,該迭代器可以按順序訪問該集合中的元素。迭代器提供 ...
  • 1. 效果展示 線上查看 2. 開始前說明 效果實現參考源碼:Logo 聚集與散開 原效果代碼基於 react jsx 類組件實現。依賴舊,代碼冗餘。 我將基於此進行重構,重構目標: 基於最新依賴包,用 ts + hook 實現效果 簡化 dom 結構及樣式 支持響應式 重構應該在還原的基礎上,用更 ...
  • vue生命周期一般為8個,特殊時期為10個 beforeCreate: //發生在頁面完成初始化,組件創建之前,數據尚未掛載 created://發生在組件創建完成時,數據已掛載,可以在此調用介面查數據,防止頁面抖動。 beforeMount://發生在組件掛載之前 mounted://發生在組件掛 ...
  • 這篇文章主要聊一下緩存,如何使用緩存來加速你的系統,減少磁碟 IO。按照讀寫性質,緩存可以分為讀寫緩存和只讀緩存,兩種緩存有各自的適用場景。 ...
  • 這篇文章主要用來討論Kafka是如何做到高性能的,包括使用批處理方式處理消息,使用順序讀寫的方式使用磁碟,利用PageCache緩存數據並減少IO操作,使用零拷貝技術加速消費流程。 ...
  • 三維模型幾何糾正方法主要包括以下幾種:坐標變換法:通過對三維模型的坐標進行變換,實現幾何糾正。常用的坐標變換包括平移、旋轉和縮放等。平移和旋轉可以通過對模型的平移和旋轉矩陣進行計算實現,縮放可以通過對模型的坐標進行縮放繫數的計算實現。點雲擬合法:將三維模型擬合到點雲數據上,通過對擬合誤差進行優化,實 ...
  • 1. G1垃圾回收器 1.1. 垃圾優先(garbage first) 1.2. 在堆內離散的區域上進行操作 1.2.1. 預設大約有2048個 1.2.2. 代的區域不需要是連續的 1.2.3. 可能屬於老年代 1.2.3.1. 併發後臺線程尋找沒有被引用的對象時,一些區域會比其他區域有更多的垃圾 ...
  • 01_GoLand debug時出現Connected並且程式卡住的問題 環境:win10、go version go1.19.4 windows/amd64、GoLand 2020.3.5 x64 現象 : 在 debug 模式下運行項目,打上斷點後,可以進入斷點位置,也可以跳轉到下個斷點,但是, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...