C++快速入門
課程推薦
這裏推薦一門課,30小時快速精通C++和外掛實戰,名字看着有點浮誇。。。但是內容真不是吹的,這裏的總結很大一部分是對於課堂內容的理解與消化。
由於C++內容太多了,這一篇文章不包含面向對象。
語法須知
- C++的源文件拓展名:cpp(c plus plus 的簡稱)
- C++程序的入口是main函數(函數即方法,一個意思)
- C++完全兼容C語言的語法,很久以前,C++叫做C with classes
cin、cout
- C++中常使用 cin、cout 進行控制檯的輸入、輸出
- endl 是換行的意思
#include<iostream>
using namespace std;
int main(){
int age;
cin >> age;
cout << "age is" << age << end1;
return 0;
}
函數重載(Overload)
C語言不支持函數重載,C++支持函數重載。
- 規則
- 函數名相同
- 參數個數不同、參數類型不同、參數順序不同
- 注意
- 返回值類型與函數重載無關
- 調用函數時,實參的隱式類型轉換可能會產生二義性
有面向對象基礎的話,應該知道個七七八八。
具體可以看看下面幾段代碼,體會一下。
#include <iostream>
using namespace std;
// 參數個數不同
int sum(int v1, int v2) {
return v1 + v2;
}
int sum(int v1, int v2, int v3) {
return v1 + v2 + v3;
}
int main() {
cout << sum(10, 20) << endl; // 30
cout << sum(10, 20, 30) << endl; // 60
return 0;
}
#include <iostream>
using namespace std;
// 參數類型不同
void func(int v1, double v2) {
cout << "func(int v1, double v2)" << endl;
}
void func(double v1, int v2) {
cout << "func(double v1, int v2)" << endl;
}
int main() {
func(10, 10.5); // func(int v1, double v2)
func(10.5, 10); // func(double v1, int v2)
return 0;
}
返回值類型與函數重載無關,爲什麼有這個設定呢?
因爲有時候會產生二義性,具體看下面這段代碼。
#include <iostream>
using namespace std;
// 歧義,二義性
int func() {
cout << "int func()" << endl;
return 1;
}
double func() { // 會報錯,函數返回值不同
cout << "double func()" << endl;
return 2.5;
}
int main(){
func(); // 不知道該調用哪個函數,自然不能構成重載
}
- 本質
- 採用了name mangling或者叫name decoration技術
- C++編譯器默認會對符號名(比如函數名)進行改編、修飾,有些地方翻譯爲“命名傾軋"
- 重載時會生成多個不同的函數名,不同編譯器(MSVC、g++)有不同的生成規則
- 通過IDA打開【VS Release禁止優化】可以看到
#include <iostream>
using namespace std;
// name mangling 技術
// C++編譯器默認會對符號名(比如函數名)進行改編、修飾
// 重載時會生成多個不同的函數名,不同編譯器(MSVC、g++)有不同的生成規則
// 命名規則與下面不一定相同,但是會產生多個函數名
// display_v
void display() {
cout << "display()" << endl;
}
// display_i
void display(int a) {
cout << "display(int) - " << a << endl;
}
// display_l
void display(long a) {
cout << "display(long) - " << a << endl;
}
// display_d
void display(double a) {
cout << "display(double) - " << a << endl;
}
默認參數
- C++允許函數設置默認參數,在調用時可以根據情況省略實參。規則如下:
- 默認參數只能按照右到左的順序
- 如果函數同時有聲明、實現,默認參數只能放在函數聲明中
- 默認參數的值可以是常量、全局符號(全局變量、函數名)
- 如果函數的實參經常是同一個值,可以考慮使用默認參數
#include <iostream>
using namespace std;
int age = 33;
void test() {
cout << "test()" << endl;
}
// 函數指針的默認參數爲test()函數
void display(int a = 11, int b = 22, int c = age, void(*func)() = test){
cout << "a is " << a << endl;
cout << "b is " << b << endl;
cout << "c is " << c << endl;
test();
}
int main(){
// 如果不傳參,會調用默認參數
display();
// display(11, 22, age, test); // 與上面完全一樣
return 0;
}
a is 11
b is 22
c is 33
test()
- 函數重載、默認參數可能會產生衝突、二義性(建議優先選擇使用默認參數)
void display(int a, int b=20){ // 有默認參數
cout<<"a is"<<a<<endl;
}
void display(int a){ //與上面的函數產生二義性
cout<<"a is"<<a<<endl;
}
int main(){
display(10); // 不知道該調用哪個函數,報錯
return 0;
}
extern “C”
- 被
extern "C"
修飾的代碼會按照C語言的方式去編譯
extern "C" void func(){
cout << "func()" <<end1;
}
extern "C" void func(int age){
cout << "func(int age)" << age << end1;
}
extern "C" {
void func(){
cout << "func()" <<end1;
}
void func(int age){
cout << "func(int age)" << age << end1;
}
}
- 如果函數同時有聲明和實現,要讓函數聲明被 extern “C” 修飾,函數實現可以不修飾
extern "C" void func(); // 函數聲明必須被 extern "C" 修飾
extern "C" void func(int age);
void func(){ // 函數實現可以不被修飾
cout << "func()" << end1;
}
void func(int age){
cout << "func(int age)" << age << end1;
}
extern"C"{
void func();
void func(int age);
}
void func(){
cout << "func()" << end1;
}
void func(int age){
cout << "func(int age)" << age << end1;
}
上面兩段代碼完全等價。
由於C、C++編譯規則的不同,在C、C++混合開發時,可能會經常出現以下操作 :
- C++在調用C語言API時,需要使用
extern "C"
修飾C語言的函數聲明
有時也會在編寫C語言代碼中直接使用extern “C”
,這樣就可以直接被C++調用
- 通過使用宏 _cplusplus 來區分C、C++環境
#pragma once
- 我們經常使用 #ifndef、#define、#endif 來防止頭文件的內容被重複包含
- #pragma once 可以防止整個文件的內容被重複包含
- 區別
- #ifndef、#define、#endif 受 CC++ 標準的支持,不受編譯器的限制
- 有些編譯器不支持 #pragma once(較老編譯器不支持,如GCC3.4版本之前),兼容性不夠好
- #ifndef、#define、#endif 可以針對一個文件中的部分代碼,而 #pragma once 只能針對整個文件
內聯函數(inline function)
-
使用 inline 修飾函數的聲明或者實現,可以使其變成內聯函數
- 建議聲明和實現都增加 inline 修飾
-
特點
- 編譯器會將函數調用直接展開爲函數體代碼
- 可以減少函數調用的開銷
- 會增大代碼體積
- 注意
- 儘量不要內聯超過 10 行代碼的函數
- 有些函數即使聲明爲 inline,也不一定會被編譯器內聯,比如遞歸函數
內聯函數與宏
- 內聯函數和宏,都可以減少函數調用的開銷
- 對比宏,內聯函數多了語法檢測和函數特性
什麼叫函數特性呢?比較下面兩個例子就知道了。
#include<iostream>
#using namespace std;
#define sum(x) (x + x)
int main(){
int a = 10;
cout << sum(a++) << endl; // 21
// 這裏的實際運算是: ((a++) + (a++))
}
#include<iostream>
#using namespace std;
inline int sum(int x){
return x + x;
}
int main(){
int a = 10;
cout << sum(a++) << endl; // 20
// 這裏的實際運算是: (a + a),然後 a += 1
}
被賦值的表達式
- C++的有些表達式是可以被賦值的
int a = 1;
int b = 2;
(a=b) = 3; //賦值給了a
(a<b?a:b) = 4; //賦值給了b
const
- const 是常量的意思,被其修飾的變量的值不可修改
- 如果修飾的是類、結構體(的指針),其成員也不可以更改
觀察一下以下指針分別是什麼含義?
int age = 10;
const int *p1 = &age; // p1不是常量,*p1是常量
int const *p2 = &age; // p2不是常量,*p2是常量
int * const p3 = &age; // p3是常量,*p3不是常量
const int * const p4 = &age; // p4是常量,*p4也是常量
int const * const p5 = &axge; // p5是常量,*p5也是常量
- 指針問題可以用以下的結論來解決:
const 修飾的是其右邊的內容
#include<iostream>
using namespace std;
int main(int argc, char *agrv[]) {
struct Date {
int year;
int month;
int day;
};
Date d1 = { 2020, 2, 17 };
Date d2 = { 2022, 4, 28 };
Date *p = &d1;
*p = d2;
cout << d1.year << endl; // 普通對象的訪問方式
cout << p->month << endl; // 指針變量的訪問方式
cout << (*p).day << endl; // 指針變量的訪問方式
int age = 10;
int height = 20;
// p1不是常量,*p1是常量
const int * p1 = &age;
// p2不是常量,*p2是常量
int const * p2 = &age;
// p3是常量,*p3不是常量
int * const p3 = &age;
// p4是常量,*p5也是常量
const int * const p4 = &age;
// p5是常量,*p5也是常量
int const * const p5 = &age;
// p5 = &height; // 報錯,無法修改
// *p5 = 20; // 報錯,無法修改
return 0;
}
引用(Reference)
- 在C語言中,使用 指針(Pointer) 可以間接獲取、修改某個變量的值
- 在C++中,使用 引用(Reference) 可以起到跟指針類似的功能
int age = 20;
int &rage = age; // rage是一個引用
- 注意點
- 引用相當於是變量的別名(基本數據類型、枚舉、結構體、類、指針、數組等,都可以有引用)
- 對引用做計算,就是對引用所指向的變量做計算
- 在定義的時候就必須初始化,一旦指向了某個變量,就不可以再改變,“從一而終"
- 可以利用引用初始化另一個引用,相當於某個變量的多個別名
- 不存在【引用的引用、指向引用的指針、引用數組】
- 引用存在的價值之一:比指針更安全、函數返回值可以被賦值
引用的本質
- 引用的本質就是指針,只是編譯器削弱了它的功能,所以引用就是弱化了的指針
- 一個引用佔用一個指針的大小
普通變量的引用
#include<iostream>
using namespace std;
int main(){
int age = 10;
// 定義了一個age的引用,ref相當於是age的別名
int &ref = age;
// 注意,這不是引用的引用,此時的ref就相當於age(別名)
int &ref1 = ref; // 所以ref1依舊是age的引用
int &ref2 = ref1; // ref2也是age的引用
ref += 10; // age += 10;
ref1 += 10; // age += 10;
ref2 += 10; // age += 10;
cout << age << endl; // 40
}
引用作爲函數的參數
先來看一段我們熟悉的代碼。
#include<iostream>
using namespace std;
void swap(int v1, int v2){
int tmp = v1;
v1 = v2;
v2 = tmp;
}
int main(){
int a = 10;
int b = 20;
swap(a, b); // a=10, b=20
cout << "a = " << a << ", b = " << b << endl;
}
我們都知道,這樣做不會真正改變 a,b 的值。
那麼改成下面這樣呢?
#include<iostream>
using namespace std;
void swap(int *v1, int *v2){
int tmp = *v1;
*v1 = *v2;
*v2 = tmp;
}
int main(){
int a = 10;
int b = 20;
swap(&a, &b); // a=20, b=10
cout << "a = " << a << ", b = " << b << endl;
}
這樣寫明顯是可以的。
其實用引用也可以達到同樣的效果,而且更簡潔。
#include<iostream>
using namespace std;
void swap(int &v1, int &v2) {
int tmp = v1;
v1 = v2;
v2 = tmp;
}
int main(){
int a = 10;
int b = 20;
// 不需要 int &ref = a; int &ref2 = b; swap(ref1, ref2);
swap(a, b); // 10, 20
cout << "a = " << a << ", b = " << b << endl;
}
結構體的引用
#include<iostream>
using namespace std;
struct Date{
int year;
int month;
int day;
};
int main(){
// 結構體的引用
Date date = {2022, 2, 2};
Date &ref1 = date; // 引用
ref1.year = 2023; // 通過引用修改了值
cout << date.year << endl; // 值發生了變化
return 0;
}
指針的引用
#include<iostream>
using namespace std;
int main(){
// 指針的引用
int age = 10;
int *p1 = &age;
int *&ref2 = p; // 引用
// 下面三種賦值是等價的
// 通過普通變量,指針,引用都可以修改值
cout << (age=30) << endl;
cout << (*p1=40) << endl;
cout << (*ref2=50) << endl;
return 0;
}
數組的引用
#include<iostream>
using namespace std;
int main(){
// 數組的引用
int *p2;
// 指針數組,數組裏面可以存放3個int型指針
int *arr1[3] = { p, p, p }; // 是數組
// 用於指向數組的指針,和上面不一樣
int(*arr2)[3]; // 是指針
// 數組的引用(引用的本質是指針,所以與指向數組的指針類似)
int array[] = { 1, 2, 3 };
int(&ref)[3] = array; // 需要寫死數組長度爲,此處爲3
int * const &ref2 = array; // 不需要寫死數組長度的引用
return 0;
}
函數返回值被賦值
- 引用存在的價值之一:比指針更安全、函數返回值可以被賦值
#include<iostream>
using namespace std;
/*
// 無法構成重載
int sum(int v1, int v2) {
cout << "sum(int v1, int v2)" << endl;
return v1 + v2;
}
*/
int sum(int &v1, int &v2) {
cout << "sum(int &v1, int &v2)" << endl;
return v1 + v2;
}
int sum(const int &v1, const int &v2) {
cout << "sum(const int &v1, const int &v2)" << endl;
return v1 + v2;
}
int main() {
// 非const實參
int a = 10;
int b = 20;
sum(a, b); // sum(int &v1, int &v2)
// const實參
const int c = 10;
const int d = 20;
sum(c, d); // sum(const int &v1, const int &v2)
sum(10, 20); // sum(const int &v1, const int &v2)
return 0;
}
常引用(Const Reference)
-
引用可以被 const 修飾,這樣就無法通過引用修改數據了,可以稱爲常引用
-
const 必須寫在 & 符號的左邊,才能算是常引用
-
const 引用 的特點
- 可以指向臨時數據(常量、表達式、函數返回值等)
- 可以指向不同類型的數據
- 作爲函數參數時(此規則也適用於 const 指針)
- 可以接受const和非const實參(非const引用,只能接受非const實參)
- 可以跟非const引用構成重載
-
當常引用指向了不同類型的數據時,會產生臨時變量,即引用指向的並不是初始化時的那個變量
#include<iostream>
using namespace std;
int main(){
int height = 20;
int age = 10;
// ref1不能修改指向,但是可以通過ref1間接修改指向的變量
int & const ref1 = age; // 寫不寫 const 其實效果一樣
ref1 = 20; // 可以通過ref1修改age的值
// ref2不能修改指向,不可以通過ref2簡潔修改指向的變量
int const &ref2 = age; // 常引用
// ref2 = 20; // 無法通過引用修改age的值,報錯
// p1不能修改指向,可以利用p1間接修改所指向的常量
int * const p1 = &age; // 實際上這就是引用
// p1 = &height; // p1不能修改指向
*p1 = 20;
// p2可以修改指向,不可以利用p2間接修改所指向的常量
int const *p2 = &age;
p2 = &height; // 可以修改指向
// *p2 = 20; // 不可以修改age的值,報錯
// 常引用可以指向臨時數據()
const int &ref1 = 30;
// int &ref2 = 50; 普通引用不可以
return 0;
}
常引用指向不同類型的數據
- 當常引用指向了不同類型的數據時,會產生臨時變量,即引用指向的並不是初始化時的那個變量
這是什麼意思呢?我們先來看一段代碼。
#include<iostream>
using namespace std;
int main() {
int age = 10;
const int &rAge = age;
age = 40;
cout << "age=" << age << endl;
cout << "rAge=" << rAge << endl;
return 0;
}
age=40
rAge=40
這是一段正常的引用,輸出的結果也在意料之內。
再看下面這段引用。
#include<iostream>
using namespace std;
int main() {
int age = 10;
const long &rAge = age; // 注意,這裏是 long 類型
age = 40; // 由於指向了不同的類型,其實修改的是一個臨時變量
cout << "age=" << age << endl; // 即,age 的值依舊是10,沒有變
cout << "rAge=" << rAge << endl; // rAge 的指是一個臨時變量的值
return 0;
}
age=10
rAge=40
體會一下上面兩段代碼的區別。