C++靈活易錯特性-02

来源:https://www.cnblogs.com/CPJzgR/archive/2018/01/26/8361649.html
-Advertisement-
Play Games

C++的四種類型轉換:static_cast、dynamic_cast、const_cast、reinterpret_cast;typedef與類型別名,作用域和一些偶爾會看到的C風格工具。 ...


C++筆記系列:靈活易錯的特性 - 02

關鍵詞:

  • 類型和類型轉換
  • typedef
  • 作用域
  • 頭文件

類型和類型轉換

typedef

typedef為已有的類型聲明提供一個新名稱。可以將typedef看成已有類型聲明的同義詞

typedef int* IntPtr

可以互換地使用新的類型名稱及其別名:

int* p1;
IntPtr p2;

從根本上,它們就是同一類型:

// 完全合法
p1 = p2;
p2 = p2;

typedef最常見的用法是當類型的聲明過於笨拙時,提供易於管理的名稱。特別是在模板當中。

如果你不想不厭其煩地寫std::vector<std::string>,就可以使用typedef

typedef std::vector<std::string> StringVector;
void ProcessVector(const StringVector& vec) {}
int main {
   StringVector my_vector;
   return 0;
}

實際上,STL廣泛地使用typedef,比如string是這樣定義的:

typedef basic_string<char, char_traits<char>, allocator<char>> string

函數指針typedef

C++中函數指針並不常見(被virtual關鍵字替代),但在某些情況下還是需要獲取函數指針。不過在定義函數指針時,typedef最讓人費解。

考慮一個動態鏈接庫(DLL),庫中有一個名為MyFunc()的函數。只有需要調用MyFunc()時,才會載入這個庫。

假定MyFunc()的原型如下,

int __stdcall MyFunc(bool b, int n, const char* p);

__stdcall時Microsoft特有的指令,指示如何將參數傳遞到函數,如何執行清理。

現在可以使用typedef為指向函數的指針定義一個縮寫名稱(MyFuncProc),該函數具有前面所示的原型:

typedef int (__stdcall *MyFuncProc)(bool b, int n, const char* p);

註意typedef的名稱MyFuncProc嵌在這個語法中間,相當費解。解決這個問題還有另外更為簡潔的方法,就是下一節將描述的類型別名

類型別名

某些情況下,類型別名比typedef更容易理解。

typedef int MyInt;

可以用類型別名寫作

using MyInt = int;

typedef變得複雜時,類型別名就特別有用。

typedef int (*FuncType)(char, double);
// 使用類型別名
using FuncType = int (*)(char, double);

類型別名不只是易於閱讀的typedef,在模板中使用typedeftypedef的問題變得很突出。

舉個例子,假定有如下的類模板:

template<typename T1, typename T2>
class MyTemplateClass {
};
// ok
typedef MyTemplateClass<int, double> OtherName;
// Error
typedef MyTemplateClass<T1, double> OtherName;
// 如果要這麼做,應該使用模板別名
template<typename T1>
using OtherName = MyTemplateClass<T1, double>;

類型轉換

C++提供了四種類型轉換:static_castdynamic_castconst_castreinterpret_cast。強烈建議再新代碼中僅使用C++風格的類型轉換,它們更安全,語法上也比C風格的()更加優秀。

1.const_cast

const_cast最直接,可以用於變數的常量特性。這是四個類型轉換中唯一可以捨棄常量特性的類型轉換。當然,從理論上講,並不需要const類型轉換。變數是const,就應該一直是const

但是,有時候某個函數需要把const變數傳遞給非const變數的函數。最好的方法當然是保持const一致,很多時候使用了第三方庫,沒有辦法,只能委曲求全,不然就只能重新構建程式。

// 第三方外部方法
extern void ThirdPartyLibraryMethod(char * str);
void f(const char* str) {
    ThirdPartyLibraryMethod(const_cast<char*>(str));
}

2.static_cast

顯示執行C++直接支持的轉換。例如,在算術表達式中,將int轉換為double避免截斷,就可以使用static_cast

int i = 3;
int j = 4;
// 只需要轉換其中一個就可以確保執行浮點數除法
double result = static_cast<double>(i) / j;

如果用戶定義了相關的構造函數或者轉換常式,也可以使用static_cast執行顯式轉換。例如,類A的構造函數將類B的對象作為參數,就可以使用static_cast將B對象轉換為A對象。很多時候都需要這種行為,編譯器一般會自動隱式執行這個轉換。

static_cast的另一種用法是在類繼承層次結構中執行向下轉換。註意,只能用於對象的指針和引用,不能轉換對象本身。

另外還需要註意,static_cast不執行運行期間的類型檢測。比如下麵代碼可能會導致災難性的後果,包括記憶體重寫超出了對象的邊界。

// Derived繼承自Base
Base* b = new Base();
Derived* d = static_cast<Derived*>(b);

要執行具有運行時檢測的更安全轉換,可以用dynamic_cast

static_cast不是萬能的,無法將const類型轉換成non-const類型,無法把某種類型的指針轉換到其它不相關類型的指針,無法直接轉換對象的類型。基本上,無法完成類型規則中沒有意義的轉換。

3.reinterpret_cast

reinterpret_caststatic_cast更強大,同時安全性更差。可以用來執行技術上不被規則允許,但某些情況下又需要的類型轉換。例如,可在兩個引用之間相互轉換,即使兩者並不相關。同樣,即使兩個指針不存在繼承關係,也可以將某種指針類型轉換為其他指針類型。

reinterpret_cast經常用於將指針轉換為void*,以及將void*轉換為指針。

class X {};
class Y {};
int main() {
    X x;
    Y y;
    X* px = &x;
    Y* py = &y;
    // 無關的指針類型間轉換隻能用reinterpret_cast
    px = reinterpret_cast<X*>(py);
    void *p = px;
    px = reinterpret_cast<X*>(p);
    X& rx = x;
    Y& ry = reinterpret_cast<Y&>(x);
}

理論上,reinterpret_cast還可以把指針轉換為int,或者把int轉換為指針。但是,這種程式可能是不正確的。許多平臺上(特別是64位),指針和int的大小不同。例如,在64位平臺上,指針是64位,整數可能是32位。將64位的指針轉換為32位整數會丟失32位。

另外,使用reinterpret_cast特別小心,它不會執行任何類型檢測。

4.dynamic_cast

dynamic_cast為繼承層次結構內的類型轉換提供運行時檢測。可用它來轉換指針或者引用。dynamic_cast在運行時檢測底層對象的類型信息。如果類型轉換沒有意義,dynamic_cast返回一個空指針(用於指針)或者拋出一個std::bad_cast異常(用於引用)。

註意運行時類型信息存儲在對象的虛表中。因此,為了使用dynamic_cast類至少要有一個虛方法。如果類沒有虛表,使用dynamic_cast會導致編譯錯誤。

下麵用於引用的dynamic_cast將拋出異常:

#include <iostream>
using std::bad_cast;
using std::cout;

class Base {
 public:
  Base() {}
  virtual ~Base() {}
};

class Derived : public Base {
 public:
  Derived() {}
  virtual ~Derived() {}
};

int main() {
    Base base;
    Derived derived;
    // 改成Base& br = derived;就不會拋出異常
    Base& br = base;
    try {
        Derived& dr = dynamic_cast<Derived&>(br);
    } catch (const bad_cast&) {
        cout << "Bad cast!\n";
    }
}

5.類型轉換總結

情形類型轉換
刪除const特性 const_cast
語言支持的類型轉換(例如,int轉換為double,int轉換為bool) static_cast
用戶自定義構造函數或者轉換常式所支持的類型轉換 static_cast
類的對象轉換為其他(無關)類的對象 無法完成
類對象的指針(pointer-to-object)轉換為同一繼承層次結構中其他類對象的指針 static_cast或者dynamic_cast(推薦)
類對象的引用(reference-to-object)轉換為同一繼承層次結構中其他類對象的引用 static_cast或者dynamic_cast(推薦)
類型的指針轉換(pointer-to-type)為其他無關類型的指針 reinterpret_cast
類型的引用轉換(reference-to-type)為其他無關類型的引用 reinterpret_cast
函數指針(pointer-to-function)為其他函數指針 reinterpret_cast

作用域解析

創建作用域可以使用

  • 名稱空間
  • 函數定義
  • 花括弧界定的塊
  • 類定義

試圖訪問某個變數、函數或者類時,會從最近的作用域開始查找這個名稱,然後時相鄰的作用域,以此類推,直到全局作用域。

如果不想用預設的作用域解析某個名稱,就可以使用作用域解析運算符::和特定的作用域限定這個名稱。例如,訪問類的靜態方法,第一種做法是將類名(方法的作用域)和作用域解析運算符放在方法名的前面。第二種方法是通過類的對象訪問這個靜態方法。

頭文件

頭文件是為代碼提供抽象介面的一種機制。使用頭文件需要註意的一點是避免迴圈引用或者多次包含同一個頭文件。最常用的方法就是使用#ifndef機制。

#ifndef可以用來避免迴圈包含和多次包含。在每個頭文件的開頭,#ifndef指令檢測是否定義了某個標記,如果標記已經定義,則代表已經包含了頭文件,編譯器將調到對應的#endif,這個指令一般位於文件的結尾。如果沒有定義該標記,將定義這個標記,重覆包含就會被忽略。這個機制也稱為頭文件保護(include guards)

#ifndef LOGGER_H
#define LOGGER_H
#include "Preferences.h"
class Logger {
 public:
  static void setPreferences(const Preferences& prefs);
  static void logError(const char* error);
};
#endif // LOGGER_H

如果編譯器支持#pragma once(Visual C++或者g++),可以這樣寫:

#pragma once
#include "Preferences.h"
class Logger {
 public:
  static void setPreferences(const Preferences& prefs);
  static void logError(const char* error);
};

前置聲明(forward declarations)是另一個避免頭文件問題的工具。如果需要使用某個類,但是無法包含它的頭文件(例如,這個類嚴重依賴當前編寫的類),就可以告訴編譯器,存在這麼一個類,但是無法使用#include。當然,不能在代碼中使用這個類,只有鏈接成功後才存在這個已命名的類。

#ifndef LOGGER_H
#define LOGGER_H
class Preference;    // 前置聲明
class Logger {
 public:
  static void setPreferences(const Preferences& prefs);
  static void logError(const char* error);
};

在大型項目中,使用前置聲明可以減少編譯和重編譯的時間,因為它破壞了一個頭文件對其它頭文件的依賴。但是,前置聲明也隱藏了依賴關係,頭文件改動時,用戶的代碼會跳過必要的重新編譯過程。

前置聲明有時候會妨礙頭文件變動API,特別是函數,例如擴大形參類型。另外,一般的項目,還沒到需要縮短編譯時間的程度。

建議函數總是用#include,類模板優先考慮#include

C的工具

C語言有一些晦澀的特性在C++中還是偶爾可以看到。比如,變長參數列表(variable length argument lists)和預處理器巨集(preprocessor macros)。

變長參數列表

在C語言中,對於這個特性一定不陌生,printf()就是使用了這種特性。

註意,新的代碼應該通過variadic模板使用類型安全的變長參數列表。

#include <cstdio>
#include <cstdarg>

bool debug = false;
void DebugOut(const char* str, ...) {
    va_list ap;
    if (debug) {
      va_start(ap, str);
      vfprintf(stderr, str, ap);
      va_end(ap);
    }
}

int main(int argc, char const *argv[]) {
    debug = true;
    DebugOut("int %d\n", 5);
    DebugOut("String %s and int %d\n", "hello", 5);
    DebugOut("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);
    return 0;
}

調用va_start()之後必須調用va_end(),確保函數結束後,堆棧處於穩定狀態。

1.訪問參數

如果想要自己訪問實際參數,可以使用va_arg()。不過,如果不提供顯式的方法,就無法知道參數列表的結尾是什麼。

可以讓第一個參數計算參數的數目,或者當參數是一組指針時,可以要求最後一個是nullptr

下麵給一個示例,調用者需要在第一個已命名的函數中指定提供的參數數目。

void PrintInts(int num, ...) {
    int temp;
    va_list ap;
    va_start(ap, num);
    for (int i = 0; i < num; ++i) {
        temp = va_arg(ap, int);
        cout << temp << " ";
    }
    va_end(ap);
    cout << endl;
}

2,為什麼不應該使用C風格變長參數列表
  • 不知道參數的數目。例如在PrintInts(),需要信任調用者傳遞了與第一個參數數目相等的參數。
  • 不知道參數的類型。

儘可能避免使用C風格的變長參數列表,傳arrayvector或者使用C++11引入的初始化列表更好。也可以通過variadic模板使用類型安全的變長參數列表。

預處理巨集

應該用內聯函數代替巨集。巨集易錯,而且不執行類型檢查。調用巨集時,預處理階段會自動展開替換,但不會真正用函數調用語義,這一行為可能導致無法預測的後果。還有,巨集很容易出錯,比如這樣寫是有問題的:

#define SQUARE(x) (x * x)

假如使用SQUARE(2 + 3),展開後變成2 + 3 * 2 + 3,計算的結果是11,不是預期中的25。正確的寫法是這樣的

#define SQUARE(x) ((x) * (x))

註意,最外面的括弧不能省略,不然可能會因為優先順序問題在算術表達式中被另行結合。

如果計算比較複雜,重覆的展開替換意味著在做重覆運算,就會有一筆不小的開銷。


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

-Advertisement-
Play Games
更多相關文章
  • 1. 指針 1.1 一個指針包含兩方面:a) 地址值;b) 所指向的數據類型。 1.2 解引用操作符(dereference operator)會根據指針當前的地址值,以及所指向的數據類型,訪問一塊連續的記憶體空間(大小由指針所指向的數據類型決定),將這塊空間的內容轉換成相應的數據類型,並返回左值。 ...
  • ...
  • 數組檢索函數 格式:array array_keys(array arr[, mixed searchValue]);以數組的形式返回arr數組中的“鍵名”,如果指定了可選參數searchValue,則只返回searchValue值的鍵名,否則arr數組中的所有鍵名都會被返回。 註意:若search ...
  • 複習默寫猜數字小代碼,出現的問題。 While語句下的條件。應為result==false和answer=input()時。如果answer=input()寫在while的外邊,就會像昨天那樣一直輸出too small。 還有就是if語句,語法錯誤,百度了一下,看起來縮進了,其實是並沒有縮進。 1、 ...
  • 1.org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.mybatis.spring.mapper.MapperScannerConfigurer#0' defined ...
  • 作者:[email protected]本文為作者原創,轉載請註明出處:https://www.cnblogs.com/oucbl/p/5940556.html 今天學習SSM框架整合,完成Spring和mybatis這兩大框架的整合做測試時候出來很多問題,主要來自於配置文件。 我這裡重點說一下Mysql數據 ...
  • 最基本命令 查看埠號:netstar -aon 殺死進程:tskill PID PHP基礎 查看埠號:netstar -aon 殺死進程:tskill PID PHP基礎 PHP程式的數據採集(獲取數據) $_GET["要獲取的名稱"]; PHP數據的輸出 echo或print:輸出的是沒有經過 ...
  • 用Struts和Hibernate完成簡單的查詢資料庫,顯示類別信息的簡單案例 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...