C++基礎學習筆記----第六課(const和引用的擴充套件、過載函式和C方式編譯的深入)
主要講了const和引用的一些擴充套件和注意事項,過載函式的和extren "C"結合使用的本質,以及引用的真正的理解方式。
const的引用
擴充套件使用方法
1.通過const引用的變數是隻讀變數,當使用const_cast經過強制型別轉換後引用將會降級為普通變數,通過改變引用的值能夠改變原來只讀變數的值,但是仍然無法直接通過改變原來的只讀變數的值,這就像換了一個名字可以變了,但是原來名字下的東西仍然不能變。
程式列印結果是兩個8,同時注意我們不能夠直接對a賦值,因為這個時候a名字下的那段記憶體仍然是隻讀的。#include <stdio.h> int main() { const int& a = 1; //const與引用結合讓一個變數變為了一個只讀變數 int& b = const_cast<int&>(a); //這裡通過const_cast來進行強制型別轉換,這個時候a就由一個 //只讀變數降級為一個普通變數被b引用。 b = 8; //a = 9; 這條語句經過編譯會報錯,因為a雖然經過了強制型別轉換,但是a仍然是一個只讀變數,不夠被賦值 printf ("%d\n",b); printf ("%d\n",a); }
2.const引用的變數是隻讀變數,這個只讀變數仍然能夠被其他的const進行引用,但是經過引用之後仍然是隻讀的,不能夠被其他常量賦值。第二次的引用可以通過const_cast去除const屬性,經過強制型別轉換以後可以通過改變這個引用的值來改變前兩個只讀變數的值。也就是說,經過兩輪最終換名字成功,這個新名字就可以改變前面連個的內容。
程式最終的列印結果是三個8#include <stdio.h> int main() { const int& a = 1; const int& b = a; b = 7; int& d = const_cast<int&>(b); d = 8; printf ("%d\n",d); printf ("%d\n",b); printf ("%d\n",a); }
同時,無論怎樣引用還是去除const屬性,這三個量的指向的地址都是一致的,例程如下:
#include <stdio.h>
int main()
{
const int& a = 1;
const int& b = a;
b = 7;
int& d = const_cast<int&>(b);
d = 8;
printf ("%p\n",&d);
printf ("%p\n",&b);
printf ("%p\n",&a);
}
3.volatile關鍵字的作用是使變數每次都從記憶體中重新取值,那麼在C++中,volatile和const進行搭配修飾一個變數的時候,const變數將不再進入符號表,而是變成一個只讀變數,通過指標可以對只讀變數的值進行更改#include <stdio.h>
int main()
{
volatile const int a = 1;
int *p = NULL;
p = const_cast<int*>(&a);
*p = 2;
printf ("%d", a);
printf ("%d", *p);
}
程式分析:上面的程式定義了一個volatile和const修飾的變數a,通過強制型別轉換將a轉換成一個變數的地址,也就是指標p,這裡通過改變指標p所指向的記憶體中的值,程式列印結果發現a的值也跟著改變了,也就是說當volatile和const進行搭配的時候,修飾的變數是一個只讀變數,不再進入符號表。
4.當定義的const常量的初始值是經過volatile和const修飾的常量賦值的,那麼這次初始的const常量也不會被放入符號表,這是C++編譯器的一種規則。
例程:
#include <stdio.h>
int main()
{
volatile const int a = 1;
int *p = NULL;
p = const_cast<int*>(&a);
*p = 2;
printf ("%d", a);
printf ("%d", *p);
const int b = a;
p = const_cast<int*>(&b);
*p = 9;
printf ("%d", b);
printf ("%d",*p);
printf ("%d",a);
}
在上面的程式中b的值是可以被改變的,也就是說這個時候b是一個只讀變數,不會進入符號表。
6.不同型別的變數初始化const引用的時候將變為一個只讀變數。
#include <stdio.h>
int main()
{
int a = 'a';
int& b = a;
const char& c = b;
b = 'b';
printf ("%c", a);
printf ("%c", b);
printf ("%c", c);
}
列印結果是bba,這裡的引用c的初始化被一個int型別的引用b來初始化,這個時候產生的c仍然是一個只讀變數。所以列印結果是bba
7.符號表
符號表是編譯器在編譯的過程中產生的關於程式中語法符號的資料結構(如常量表、變數名錶、陣列名錶、函式名錶等),符號表是編譯器自己使用的內部資料結構,和我們所寫的程式本身並沒有關係,符號表不會進入最終產生的可執行程式中。
const和引用的總結
1.只有字面量初始化的const常量才會進入符號表,也就是變為真正意義上的常量。①對const常量進行引用會導致編譯器為其分配空間
②雖然const常量被分配了空間,但是這個空間中的值並不會被真正的使用
③使用其他變數初始化的const常量仍然是隻讀變數
2.被volatile修飾的const常量不會進入符號表
被volatile修飾的常量退化為只讀變數,每次訪問都要從記憶體中重新取值。
3const引用型別與初始化變數的分為兩個方面
①如果這兩個型別相同,初始化變數將變為只讀變數
②如果這兩個型別不同,將生成一個新的只讀變數,其初始值與初始化變數相同
引用和指標
引用和指標的區別
指標是一個變數,其值是一個記憶體地址,通過指標可以訪問指標對應的記憶體地址中的值。引用是一個變數的新名字,所有對引用的操作都會傳遞到引用的變數上。指標可以被const修飾成為常量或只讀變數。const引用使其引用的變數具有隻讀屬性。指標就是變數,不需要初始化,也可以指向不同的地址。引用必須在定義的時候就初始化,之後無法引用其他變數。因為引用在C++編譯器的內部實現機制是指標產量,所以說必須進行初始化。那麼你說引用是不是對指標常量的一個封裝呢?哈哈~胡拆的哈~
如何理解引用的本質就是指標常量
首先,引用的本質就是指標常量,這是C++的一種設計模式,這和為什麼C語言中有指標都是一個型別的問題,因為語言的設計嘛。
從C++語言角度看,引用和指標常量沒有任何關係,引用時變數的新名字,操作引用就是操作對應的變數。
從C++編譯器的角度看,為了支援新概念“引用”必須有一個有效的解決方案,在編譯器內部使用指標常量來實現引用,因此引用在定義時必須初始化。
一個學習過程中的例程:
#include <stdio.h>
struct SV
{
int x;
int y;
int z;
};
struct SR
{
int& x;
int& y;
int& z;
};
int main()
{
SV sv = {1, 2, 3};
SR sr = {sv.x, sv.y, sv.z};
printf("&sv = %p\n", &sv);
printf("&sv.x = %p\n", &sv.x);
printf("&sv.y = %p\n", &sv.y);
printf("&sv.z = %p\n", &sv.z);
printf("&sr = %p\n", &sr);
printf("&sr.x = %p\n", &sr.x);
printf("&sr.y = %p\n", &sr.y);
printf("&sr.z = %p\n", &sr.z);
SV& rsv = sv;
rsv.x = 4;
rsv.y = 5;
rsv.z = 6;
printf("sv.x = %d\n", sv.x);
printf("sv.y = %d\n", sv.y);
printf("sv.z = %d\n", sv.z);
return 0;
}
過載和C方式編譯
例程:
#include <stdio.h>
void func(int a, int b)
{
printf("1\n");
}
void func(int a, char b)
{
printf("2\n");
}
void func(char a, int b)
{
printf("3\n");
}
void func(char a, char b)
{
printf("4\n");
}
int main()
{
int a = 3;
char b = 'a';
func(a, a);
func(a, b);
func(b, a);
func(b, b);
return 0;
}
上面的程式可以進行順利編譯,這在前面的博文也說過,如果多個函式進行過載,且函式名相同,那麼C++編譯器會根據函式的引數型別進行匹配來呼叫函式。
例程:
#include <stdio.h>
void func(int a, int b)
{
printf("1\n");
}
void func(int a, char b)
{
printf("2\n");
}
void func(char a, int b)
{
printf("3\n");
}
void func(char a, char b)
{
printf("4\n");
}
int main()
{
int a = 3;
char b = 'a';
func(a, a);
func(a, b);
func(b, a);
func(b, b);
func(3, a);
func(3 'a');
func('a', 3);
func('a', '3');
return 0;
}
上面的程式在VS2008的編譯環境將能夠編譯通過,但是如果換作其他編譯器並能保證(手上除了VS沒有其他的了,所以只能聽老師的了)。
C++對字面常量的處理方式
整數型字面量的預設型別為int,佔用4個位元組。浮點型字面量的預設型別為double,佔用8個位元組。字元型字面量的預設型別為char,佔用1個位元組。字串型字面量的預設型別為constchar*,佔用4個位元組。當使用字面常量對對變數進行初始化或者賦值的時候,編譯器會根據變數的型別和字面量進行比較,如果左面的變數能夠容下右面的值,那麼將不會產生溢位如果左面的值不能夠容下右面的值,將會產生溢位和截斷。
函式過載的深度規則
①精確匹配實參
②通過預設型別轉換匹配實參
③通過預設引數匹配實參
三條規則會同時對已存在的過載函式進行挑選 當實參為變數並能夠精確匹配形參時,不再進行預設型別轉換的嘗試。 當實參為字面量時,編譯器會同時進行精確匹配和預設型別轉換的嘗試。C方式編譯
extern ”C“ 通知C++編譯器將其中的程式碼進行C方式的編譯,C方式的主要指按照C語言的規則對函式名進行編譯,函式名通過編譯可能與原始碼中的名字有所不同,C++編譯器為了支援過載,函式名經過編譯後會加上引數資訊,因而編譯後的函式名與原始碼中的完全不同。C編譯器不會在編譯後的函式名中加上引數資訊。extern “C”中的過載函式經過C方式編譯後將得到相同的函式名,因此extern “C”中不允許過載函式,但extern “C”中的函式可以與extern “C”之外的函式進行過載。
例程如下:
#include <stdio.h>
extern "C"
{
void func(int n)
{
const int a = 1;
int& b = const_cast<int&>(a);
b = 5;
printf("i = %d\n", a);
printf("ri = %d\n", b);
}
}
void func(const char* s)
{
printf("%s\n", s);
}
int func(int a, int b)
{
return a + b;
}
int main()
{
func(1);
func("abc");
func(1, 2);
return 0;
}
#include <stdio.h>
extern "C"
{
void func(int n)
{
const int a = 1;
int& b = const_cast<int&>(a);
b = 5;
printf("i = %d\n", a);
printf("ri = %d\n", b);
}
int func(int y, int z)
{
return y;
}
}
void func(const char* s)
{
printf("%s\n", s);
}
int func(int a, int b)
{
return a + b;
}
int main()
{
func(1);
func("abc");
func(1, 2);
return 0;
}
上面的程式將會編譯報錯,因為在extern ”C“中我們又對func函式進行了過載,這個時候通過C方式編譯出來的程式將會有兩個沒有引數資訊的函式func,所以報錯。