理解C++疑難問題
1. 引用
專業的C++代碼都大量使用了引用。C++的引用是另外一個變量的別名。對引用的修改都會改變該引用所指向變量的值。可以把引用看成是一種隱式的指針,它可以免除獲取變量地址和對指針解除引用的麻煩。也可以把引用看作是原變量的另一個名字。可以創建獨立的引用變量、使用類中的引用數據成員、接受作為傳遞給函數和方法的參數、從函數和方法返回引用。
(1).引用變量:必須在創建時對其初始化。
int x = 3;
int &xRef = x;
xRef = 10;//x的值修改為10
如果在類外聲明的引用變量而不初始化,這是不允許的。
int &yRef; //error
必須在分配引用時對其初始化。通常,引用是在聲明是分配的,不過引用數據成員可以在包含該成員的類的初始化列表中進行初始化。
除非引用指向一個const值,否則不能創建指向未命名值的引用。
int &unnameRef = 5 ; //error
const int &unnameRef = 5; ok
引用總是指向初始化時指定的那個變量。一旦創建引用,就不能再修改了。
int x=3,y=5;
int &xRef = x;
xRef = y; //xRef引用沒有指向y,仍然是指向x,只是x的值修改成y的值5了。
你或許希望在賦值時取y的地址來繞過這條限制:
int x=3,y=5;
int &xRef = x;
xRef = &y; //error ,y的地址是指針,而xRef是int變量的引用,而不是指針的引用
對於下面會怎麼樣呢?
int x=3,y=5;
int &xRef = x;
int &yRef = y;
xRef = yRef; //xRef引用沒有指向y,仍然是指向x,只是x的值修改成y的值5了。
綜上所述:引用在初始化後就不能修改而指向別的變量,只能修改其指向的變量的值。
指針引用和引用指針:
下面是一個指向int指針的引用的例子
int *intP;
int *&ptrRef = intP
ptrRef = new int;
*ptrRef = 5;
注意,取引用的地址和其引用所指向的變量的地址,這二者結果是一樣的。
int x =3;
int &xRef = x;
int *xPtr = &xRef;//等價於int *xPtr = &x
*xPtr = 100;
注意:不能聲明指向引用的引用,也不能聲明引用指針(指向引用的指針)。
int x =3;
int &xRef = x;
int &&doubleRef = xRef;//error
int &*refPtr = &xRef;//error
(2).引用數據成員:
類的數據成員可以是引用。但是如果引用不指向其他某個變量,這樣的引用是無法存在的。因此,必須在構造函數初始化列表中初始化引用數據成員,而不是在構造函數體中完成初始化。
(3).引用參數:
C++不常使用獨立的引用變量或者引用數據成員。引用最通常的用法是作為函數和方法的參數。
void swap(int& first,int& second)
{
int temp = first;
first = second;
second = temp;
}
而下面的函數達不到效果
void swap(int first,int second)
{
int temp = first;
first = second;
second = temp;
}
我們知道不能使用常量來初始化引用變量,與此類似,不能把常量作為實參傳遞給采用傳引用為參數的函數。
swap(3,5); //error
來自指針的引用:如果把一個指針傳遞給函數或方法,而該函數或方法需要的是一個引用。在此種情況下,簡單地對指針進行解除引用,從而把指針轉化為引用。
int x = 3,y=5;
int *xp = &x,*yp = &y;
swap(*xp,*yp);
傳值和傳引用:
如果想修改參數,並希望這些修改反映到函數或方法的實參變量上,此時就應該采用傳引用。但是,不應該限制為只是在這種情況下才采用傳引用。傳引用可以避免復制函數實參,在某些情況下能帶來二個好處:
a. 效率。復制大的對象和結構時可能會花費很長的時間。傳引用只向函數或方法傳遞指向對象或結構的指針。
b. 正確性。不是所有的對象都允許傳值。即使允許傳值,也不見得就能正確地支持深復制。我們知道要支持深復制,有動態分配內存的對象必須提供定制的復制構造函數。
如果想發揮這二個優點,同時不想改變原來的對象,可以在前面加上const。
傳引用的這些優點意味著,對於簡單內置類型,不需要修改實參,就應當使用傳值。在其他的情況下可以考慮傳引用。
(4).引用返回類型:
從函數或方法返回引用。這樣做的主要原因是出於效率的考慮。不是返回一個完整的對象,而是從函數或方法返回對象的引用,這樣記憶可以避免不必須要的復制。當然,只能當前對象在函數或方法結束仍然存在才可以使用此技術。
注意:必要返回函數或方法中在棧上創建的變量的引用。因為函數或方法在結束時會撤銷這些變量。函數中在堆上分配的變量在函數結束時會撤銷嗎?
(5).采用引用還是指針:
C++中的引用大概是多余的,引用可以做的,幾乎指針都可以做。
不過引用比指針編寫的代碼要清晰一些,也要安全,不可能存在無效的引用,不需要明確地解除引用,所以不會遇到指針可能存在的解除引用錯誤。
需要改變指著指向的位置的情況下,需要使用指針。
要看參數和返回類型中是采用指針還是采用引用合適,有一種方法,就是考慮誰擁有內存。如果收到變量的代碼要負責釋放與對象關聯的內存,就必須接受對象的指針。如果收到變量的代碼不必釋放內存,就應該接受變量引用。即,除非需要動態分配內存或者要在其他地方改變或釋放指針指向的值,否則,都應當使用引用而不是指針。(這條規則也適用於獨立變、函數或方法參數、函數或方法返回值。
關鍵字疑點:
1.const關鍵字:
指定或者要求其聲明的變量不變。
const有二種不同但相關的用法,一種標識變量,一種標識方法。
a.const變量:聲明此變量不能修改。可以把任何變量標識為const,包括全局變量和類的數據成員。也可以使用const來指定函數或方法的參數應該保持不變。
const double PI = 3.14159;//等價於#define PI 3.14159
const指針:
int x = 5;
const int * p = &x; //不能通過指針p來修改x的值,但是可以通過x自身來修改。
*p = 10;//error
x =10; //ok
int const *p = &x; //等價於const int * p = &x
對於:
int x = 5,y = 8;
int *const p = &x;//可以通過p來修改x的值,但是不能修改p指向的對象了。
*p = 10;//ok
p = &y; //error
既然不能修改p本身,所以需要在聲明p時對其初始化。
對於:
const int * const p = &x;// 既不能通過指針p來修改x的值, 也不能修改p指向的對象。
b.const 引用:
應用於引用的const關鍵字通常比應用於指針const關鍵字要簡單。原因有二,一:引用默認就是const的,也就是說不能修改它們指示的變量(即不能讓它再指示別的變量)。所以,C++不允許顯式地用const來標識引用變量(即如,int & const xRef = x)。二:引用一般只是一個間接層。不能創建對引用的引用。要得到多重間接層(間接引用),唯一的辦法就是創建指針的引用。
因此,我們談到的const引用,其實是指:
int z;
const int &zRef = z; 等價於int const &zRef = z;
zRef = 4; //error
z = 4;//ok
const引用最常見就是作為函數或方法的參數。
注意:把對象作為參數傳遞時,默認的做法應該是傳遞const引用。只有確實需要改變傳遞過來的對象時才應該去掉const。
c.const方法
用來聲明方法不能修改類中不可變的數據成員。
2. 關鍵字static
C++中的static關鍵字有三種,而且看起來不相關的用法。
a.static數據成員和方法
它們不屬於某個對象,而是屬於這個類。
b.static連接
C++每個源文件都是獨立編譯的,得到的對象文件要連接在一起。C++源文件中的每個名字,包括函數和全局變量,都有一個連接,可能是內部(internal)連接,也可能是外部(external)連接。外部連接是指,對於其他源文件,這個名字是可用的。內部連接(也稱為靜態連接(static linkage))是指,對於其他源文件,這個名字不可用。函數和全局變量默認都有外部連接。但是,可以在聲明前面加上關鍵字static,來指定內部(靜態)連接。
//FirstFile.cpp
void f();
int main()
{
f();
return 0;
}
給出了f()的原型,但沒有定義
// AntherFile.cpp
#include<iostream>
using namespace std;
void f();
void f()
{
cout<<”f\n”<<endl;
}
給出了f()的原型和定義。
需要說明的是,在二個不同的文件中編寫同一個函數的原型是合法的。如果每個源文件都用#include包含了一個頭文件,並把方法的原型放在這個頭文件中,預處理所做的正是這個工作,其作用就是在不同的源文件中有同一個方法的原型。使用頭文件的原因是維護原型的副本(並保持同步更新)更為容易。不過,對於這個例子沒有使用頭文件。
這些文件編譯與連接都能通過,因為f()有外部連接,main()函數可以從不同的文件調用它。
然而,如果在AntherFile.cpp文件中對方法f()使用static關鍵字:
// AntherFile.cpp
#include<iostream>
using namespace std;
static void f();
void f()
{
cout<<”f\n”;
}
現在,盡管編譯每個源文件時都沒有問題,但是連接不會成功,因為方法f()使用內部鏈接,這樣源文件FirstFile.cpp中就不能使用這個方法了。定義了static方法,但是在源文件中沒有使用,有些編譯器會發出警告。
注意,此時在f()的定義前面不需要重復關鍵字static。
要達到上訴的內部(靜態)連接的效果,還有一種方法就是:采用匿名命名空間。即把變量和函數包裝在一個未命名的命名空間中,而不是使用static關鍵字。
//AntherFile.cpp
#include<iostream>
using namespace std;
namespace {
void f();
void f()
{ cout<<”f\n”;}
}
聲明了匿名命名空間中的實體之後,可以在同一源文件中的任意位置訪問這些實體,但是在其他的源文件中不能訪問。
c. 函數中的static變量
此種用法是創建局部變量,只在進入和退出變量作用域之間維護變量的值。函數內部的靜態變量就像只能是只能從該函數訪問的全局變量一樣。靜態變量的一種通常用法是“記住”是否都有已經為一個函數完成特定的初始化。
void performTask()
{
static bool inited = false;
if(!inited)
{ cout<<”initing\n”; inited = true;}
}
然而,static變量往往讓人很糊塗,通常還有更好的方法來建立代碼,而避免使用static變量。在這種情況下,可能想在編寫類時,編寫一些構造函數來完成所需的初始化工作。
要避免使用獨立的static變量。應在對象內維護變量狀態。
3. 關鍵字extern
它看起來與static相對立的,extern用來為聲明外部鏈接。比如,對於const與typedef默認的都有內部鏈接,所以可以用extern為其指定外部鏈接。
把一個名字指定為extern時,編譯器會把它當作聲明而不會定義來對待。意味著編譯器不會為其分配空間。必須為變量提供沒有關鍵字extern的另外的定義。
//AntherFile.cpp
extern int x;
int x = 3; //等價於extern int x = 3;
上面的文件中可以不用extern,因為x默認的就有外部鏈接。
在下面文件中使用
//FirstFile.cpp
#include<iostream>
using namespace std;
extern int x;
int main()
{ cout<<”x = ”<<x<<endl; return 0;}
如果此文件不用extern會導致連接失敗,因為全局作用域內有二個x變量。
不過我們建議,盡可能不要使用全劇變量。全局變量容易讓人迷惑,也很容易出錯,尤其是在大型程序中。要完成這樣一些功能,應該使用static類成員和方法。
4. 非局部變量的初始化順序
程序中的全局變量和static類數據成員都是在main()函數開始運行前初始化的。給定源文件,會按照它們在該文件中出現的順序初始化的。
然而,C++並沒有指定也不能保證不同源文件中非局部變量的初始化順序。
類型和類型強制轉換:
typedef
為已有類型提供了一個新的名字。而並沒有創建新類型-只是提供了引用原類型的新方法。
最常見的用法就是,當實際的類型名很麻煩的時候可以為其提供一個可管理的名字。
類型強制轉換:
在C中使用()進行強制轉換,C++中提供了四種新的類型強制轉換方法:static_cast、dynamic_cast、const_cast、reinterpret_cast。應該多使用C++風格的類型強制轉換。因為C++風格的類型強制轉換會完成更多的類型檢查。
const_cast:可以去除變量的常量性。這是這四種中唯一允許去除變量的常量性的類型強制轉換。從理論上講,應該不會需要進行const類型強制轉換。如果變量聲明為const,應該保持其不變。但是有時發現這種情況:函數指定一個const變量,但是接著這個const變量必須傳遞給一個取非const變量的函數。正確的做法是在程序中保持const的一致性,但是並不能總是這樣,尤其是使用第三方的庫時更是這樣。因此,有時需要用這種類型強制轉換。
void g(char *str)
{}
void f(const char *str)
{ g(const_cast<char*>(str));}
static_cast:
可以使用static_cast來顯示完成C++語言直接支持的轉換。
int i = 3;
double result = static_cast<double>(i);
也可以使用static_cast來顯示地完成用戶定義構造函數或者轉換例程所允許的轉換。
比如:類A有一個構造函數,這個構造函數取類B的一個對象,那麼可以使用static_cast把B對象轉換為一個A對象。然而,在需要進行這種轉換的大部分情況下,編譯器都會自動完成轉換。
另一種用法是在繼承層次結構中完成向下類型強制轉換。
class Base
{
public:
Base(){}
virtual ~Base(){}
};
class Derived:public Base
{
public:
Derived(){}
virtual ~Derived(){}
};
int main()
{
Base *b;
Derived *d = new Derived();
b = d; //會自動向上轉換
d = static_cast<Derived*>(b); //需要提供static_cast
Base base;
Derived derived;
Base & br = base;
Derived& dr = static_cast<Derived&>(br);
return 0;
}
這種類型強制轉換可以應用於指針,引用,但是不能處理對象本身。static_cast這種轉換也不會完成運行時類型檢查。
static_cast不能直接把一種類型的指針轉換為另一種無關的類型。不能使用static_cast把指針轉換為int。不能使用static_cast直接把一種類型的對象轉換為另一種對象。不能使用static_cast把一個const類型強制轉換為非const類型。任何沒有意義的轉換,static_cast都做不到。
reinterpret_cast:功能比static_cast強,但安全性更低。
可以把一種類型的指針強制轉換為另外一種類型的指針,即使在繼承結構它們之間不相關也可以。類似的,可以把一種類型的引用強制轉換為另外一種類型的引用,即使這二種引用不相關也可以。還可以把指針轉換為int,或者把int轉換為指針。使用reinterpret_cast時要格外小心,因為它會把原始的位解釋為不同類型,而不完成任何類型檢查。
dynamic_cast:使用dynamic_cast進行類型強制轉換時,會在繼承層次結構中對類型強制轉換完成類型檢查。可以使用dynamic_cast來對指針或引用進行強制類型轉換。dynamic_cast會在運行時檢查底層對象的運行時類型信息。如果類型強制轉換沒有意義,dynamic_cast會返回NULL(對指針轉換),或者拋出bad_cast異常(對於引用轉換)。
class Base
{
public:
Base(){}
virtual ~Base(){}
};
class Derived:public Base
{
public:
Derived(){}
virtual ~Derived(){}
};
int main()
{
Base *b;
Derived *d = new Derived();
b = d; //會自動向上轉換
d = dynamic_cast<Derived*>(b); //需要提供dynamic_cast
Base base;
Derived derived;
Base & br = base;
try{
Derived& dr = dynamic_cast<Derived&>(br);
}catch(bad_cast&){cout<<” bad_cast!”<<endl;}
return 0;
}
作用域解析操作符:
首先在最內層檢查要訪問的的名字,然後再逐漸向外,直到全局作用域。不再任何名空間、函數或者類中的名字都在全局作用域中。
有時候,一些作用域中的名字會隱藏其他作用域中同樣的名字。
還有時候,你在此作用域中不是想訪問默認作用域的此名字,而是想訪問別的作用域同樣的名字,就要使用作用域解析操作符::,為每個名字限定一個作用域。
注意:全局作用域是未命名的,所以如果要訪問全局作用域中的名字,就直接單獨使用::,而不需要在前面加上作用域名稱。
頭文件:頭文件中要避免同一個文件的循環引用和多重包含。使用#ifndef機制可以用於避免循環包含和多重包含。
//logger.h
#ifndef __LOGGER__
#define __LOGGER__
#include “Preferences.h”
class Logger{};
#endif //__LOGGER__
要避免頭文件的這些問題,另一種做法就是超前引用。
C中實用的工具
1. 變長函數參數列表 如:print()
void debugOut(char *str,...);
...表示任意數量和類型的參數。要訪問這些參數,必須使用在<cstdarg>中定義的宏。可以聲明va_list類型的變量,並通過調用va_start()來初始化該變量。va_stat()的第二個參數必須是參數列表中最右邊的命名變量。所有函數都至少需要一個命名參數。在這個參數結束之後,它調用va_end()來結束對變長參數列表的訪問。在調用va_start()之後必須調用va_end()來確保函數調用棧最後保持一致狀態。
盡量不用此方法:因為不知道參數個數,不知道參數類型。
2. 預處理宏
#define SQUARE(x) ((x)*(x)) //注意預處理宏一定多用小括號
作為經驗,盡量不用宏取代內聯。很容易出錯,不進行類型檢查,還可能會帶來調試錯誤(因為你編寫的代碼不是編譯器看到的代碼)。