@TOC
📝內聯函數
內聯函數是一種編譯器優化技術,它可以將函數的代碼直接插入到函數調用的地方,而不是通過函數調用的方式。這樣可以減少函數調用的開銷,提高程序的執行效率。
舉個例子,當你在一個項目中,想要頻繁調用一個Add函數
int Add(int x, int y)
{
return x + y;
}
當你調用一千次,一萬次,函數棧幀相應的要建立這麼多次,對於代碼空間和時間考慮,消耗大,也浪費。 此時,你肯定在想到C語言中的宏來在代碼進行預處理解決
#define ADD(a, b) ((a) + (b))
當然你也要注意括號問題,在使用宏定義時需要格外小心,因為宏定義是在編譯時進行替換的,如果定義不當可能會導致一些意料之外的行為,避免出現以下有關括號寫法問題:
#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)
#define ADD(int a, int b) return a + b;
這個宏定義是錯誤的。宏定義中不能包含 return 語句,因為宏展開時會直接替換代碼,而不是像函數那樣有返回值。
代碼測試
#define _CRT_SECURE_NO_WARNINGS 1
#define ADD(a, b) ((a) + (b))
#include <stdio.h>
int main()
{
int ret = ADD(1, 2);
printf("%d\n", ADD(1, 2));
int x = 1, y = 2;
printf("%d\n", ADD(x & y, x | y));
return 0;
}
於C++中,在函數聲明前增加inline 關鍵字來告訴編譯器這個函數為內聯函數:
inline int Add(int a, int b)
{
return a + b;
}
以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數調用建立棧幀的開銷,內聯函數提升程序運行的效率。當編譯器編譯運行到內聯函數,將會把函數調用的代碼,直接替換,不需要再去Call該函數的地址,然後再通過這個函數的地址去尋找函數的代碼,這樣可以避免函數調用時建立棧幀的開銷,提高程序的運行效率。這在反彙編中彙編代碼中操作中有所體現,讓我們看看:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
inline int Add(int begin, int end)
{
return begin + end;
}
int main()
{
int ret = 0;
ret = Add(6, 8);
return 0;
}
例子中,代碼的在前面原本的代碼中加多了一個inline,因此這個函數是內聯函數,編譯器會將main()函數中的Add(6,8)先把6放在在寄存器eax中,然後讓8與寄存器eax進行相加,這樣的直接操作就實現了函數的相加,相比調用函數開銷,提高了程序的運行效率。
這是反彙編對比圖:
🌠 查看內聯函數inline方式
查看內聯函數的方式確實需要根據編譯模式的不同而採取不同的方法:在 Visual Studio 2019 中,查看內聯函數的步驟如下:
- 在 Debug 模式下:
- 找到項目 -> 屬性
- -> C/C++ -> 常規-> 調試信息格式 -> 程序數組路庫(/Zi) -
- -> 選完上面再接着 -> 優化-> 內聯函數擴展 -> 直適用於_inline(/Ob1)
- 點擊確定,然後按下F10,右擊鼠標找到反彙編,即可。
- 這樣在 Debug 模式下也能看到內聯函數被展開的彙編代碼
- 在 Release 模式下:
- 同樣可以查看生成的彙編代碼,如果沒有看到對應的
call指令,就説明該函數被內聯展開了
另外,Visual Studio 2019 還提供了一個更直觀的方式來查看內聯函數的情況:
- 在代碼編輯器中,將鼠標懸停在內聯函數的調用處,Visual Studio 會彈出一個提示框,顯示該函數是否被內聯展開。
🌉內聯函數特性
inline是一種以空間換時間的做法,如果編譯器將函數當成內聯函數處理,在編譯階段,會用函數體替換函數調用,缺陷:可能會使目標文件變大,優勢:少了調用開銷,提高程序運行效率。inline對於編譯器而言只是一個建議,不同編譯器關於inline實現機制可能不同,一般建議:將函數規模較小(即函數不是很長,具體沒有準確的説法,取決於編譯器內部實現)、不是遞歸、且頻繁調用的函數採用inline修飾,否則編譯器會忽略inline特性。下圖為《C++prime》第五版關於inline的建議:inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址了,鏈接就會找不到。- F.h
#include <iostream>
using namespace std;
inline void f(int i);
- F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
- test.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
鏈接錯誤:main.obj : error LNK2019: 無法解析的外部符號 "
cpp void __cdecl f(int)" (?f@@YAXH@Z),該符號在函數_main中被引用
🌉面試題
宏的優缺點? 優點: 1.增強代碼的複用性。 2.提高性能。 缺點: 1.不方便調試宏。(因為預編譯階段進行了替換) 2.導致代碼可讀性差,可維護性差,容易誤用。 3.沒有類型安全的檢查 。
C++有哪些技術替代宏?
- 常量定義 換用
constenum - 短小函數定義 換用內聯函數
🌠auto關鍵字(C++11)
在早期C/C++中auto的含義是:使用auto修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它,大家可思考下為什麼?
C++11中,標準委員會賦予了auto全新的含義即:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。簡言之:使用 auto 聲明變量時,編譯器會自動推導出變量的類型,無需顯式指定。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto x = 42; // x 的類型被推導為 int
auto y = 3.14; // y 的類型被推導為 double
auto z = "hello"; // z 的類型被推導為 const char*
auto d = TestAuto();
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 無法通過編譯,使用auto定義變量時必須對其進行初始化
return 0;
}
注意:使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類型。因此auto並非是一種“類型”的聲明,而是一個類型聲明時的“佔位符”,編譯器在編譯期會將auto替換為變量實際的類型。
🌠 auto的使用細則
auto與指針和引用結合起來使用用auto聲明指針類型時,用auto和auto*沒有任何區別,但用auto聲明引用類型時則必須加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
- 在同一行定義多個變量當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進行推導,然後用推導出來的類型定義其他變量。
int TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因為c和d的初始化表達式類型不同
return a + b + c + d;
}
int main()
{
auto d = TestAuto();
cout << typeid(d).name() << endl;
return 0;
}
🌉auto不能推導的場景
- auto不能作為函數的參數
// 此處代碼編譯失敗,auto不能作為形參類型,因為編譯器無法對a的實際類型進行推導
int TestAuto(auto a)
{
return a = 1;
}
int main()
{
auto d = TestAuto(2);
cout << typeid(d).name() << endl;
return 0;
}
auto不能直接用來聲明數組auto關鍵字確實不能直接用來聲明數組。這是C++語言的一個特性限制。
在 C++ 中,數組是一種特殊的數據結構,它的大小和元素類型在編譯時就必須確定。而 auto 關鍵字是用來進行類型推導的,它無法推導出數組的大小和元素類型。
所以,下面的代碼是無法編譯通過的:
auto arr[] = {1, 2, 3, 4, 5}; // 錯誤: 無法使用 auto 推導數組類型
正確的做法是使用顯式的類型聲明:
int arr[] = {1, 2, 3, 4, 5};
或者使用 std::array 容器,它可以與 auto 關鍵字配合使用:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
auto arr2 = arr; // arr2 的類型被推導為 std::array<int, 5>
- 為了避免與
C++98中的auto發生混淆,C++11只保留了auto作為類型指示符的用法 auto在實際中最常見的優勢用法就是跟以後會講到的C++11提供的新式for循環,還有lambda表達式等進行配合使用。
🌠基於範圍的for循環(C++11)
🌠範圍for的語法
在C++98中如果要遍歷一個數組,可以按照以下方式進行:
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << " ";
}
cout << endl;
return 0;
}
對於一個有範圍的集合而言,由程序員來説明循環的範圍是多餘的,有時候還會容易犯錯誤。因此C++11中引入了基於範圍的for循環。for循環後的括號由冒號“ :”分為兩部分:第一部分是範圍內用於迭代的變量,第二部分則表示被迭代的範圍。
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++11 範圍for
// 自動取數組array中,賦值給e
// 自動++,自動判斷結束
for (auto& e : array)
{
e *= 2;
}
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
注意:與普通循環類似,可以用
continue來結束本次循環,也可以用break來跳出整個循環。
🌉 範圍for的使用條件
- for循環迭代的範圍必須是確定的對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的方法,begin和end就是for循環迭代的範圍。 注意:以下代碼就有問題,因為for的範圍不確定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
因為 array 在這裏是一個指針,而不是一個數組。使用範圍 for 循環遍歷指針是不合適的,因為循環的範圍是未知的。
- . 對於數組而言,數組的大小在編譯時就已經確定了,所以我們可以直接使用數組的首地址和末地址作為迭代範圍:
void TestFor(int array[], size_t size)
{
for (size_t i = 0; i < size; ++i)
std::cout << array[i] << std::endl;
}
- 對於類而言,如果想使用範圍 for 循環,則需要提供
begin()和end()成員函數,返回指向容器首尾元素的迭代器:
class MyContainer {
public:
int* begin() { return data; }
int* end() { return data + size; }
// ...
private:
int data[10];
size_t size;
};
void TestFor(MyContainer& c)
{
for (auto& e : c)
std::cout << e << std::endl;
}
- 迭代的對象要實現++和==的操作。(關於迭代器這個問題,以後會講,現在提一下,沒辦法講清楚,現在大家瞭解一下就可以了)
🌉 指針空值nullptr(C++11)
🌠C++98中的指針空值
在良好的C/C++編程習慣中,聲明一個變量時最好給該變量一個合適的初始值,否則可能會出現不可預料的錯誤,比如未初始化的指針。如果一個指針沒有合法的指向,我們基本都是按照如下方式對其進行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL實際是一個宏,在傳統的C語言頭文件(stddef.h)中,可以看到如下代碼:
#ifndef NULL//這是一個預處理指令,檢查是否已經定義了 NULL 宏。如果沒有定義,則執行下面的代碼塊。
#ifdef __cplusplus//這個預處理指令檢查是否在 C++ 編譯環境下。如果是 C++ 編譯環境,則執行下面的代碼塊。
#define NULL 0//在 C++ 編譯環境下,將 NULL 宏定義為 0。這是因為在 C++ 中,0 可以隱式轉換為任何指針類型,所以將 NULL 定義為 0 是合理的
#else//如果不是 C++ 編譯環境,則執行這個代碼塊。
#define NULL ((void *)0)//在 C 語言編譯環境下,將 NULL 宏定義為(void *)0。這裏使用 (void *) 進行強制類型轉換,將整數 0 轉換為 void * 類型,這樣可以表示一個空指針
#endif//結束 #ifdef __cplusplus 的條件編譯塊
#endif//結束 #ifndef NULL 的條件編譯塊
可以看到,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量。不論採取何種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩,比如:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通過f(NULL)調用指針版本的f(int*)函數,但是由於NULL被定義成0,因此與程序的初衷相悖。在C++98中,字面常量0既可以是一個整形數字,也可以是無類型的指針(void*)常量,但是編譯器默認情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進行強轉(void *)0。
注意:1. 在使用
nullptr表示指針空值時,不需要包含頭文件,因為nullptr是C++11作為新關鍵字引入的。2. 在C++11中,sizeof(nullptr)與sizeof((void*)0)所佔的字節數相同。3. 為了提高代碼的健壯性,在後續表示指針空值時建議最好使用nullptr