1. 程式人生 > 實用技巧 >【CPP】CPP 基礎

【CPP】CPP 基礎

CPP 基礎

C 語言基礎

C 語言關鍵字

char, short, int, long, float, double, struct, union, enum, void, const, signed, unsigned, sizeof, typedef, auto, static, register, extern, if, else, switch, case, default, for, while, do, break, continue, return

基本資料型別

  • char, short, int, long, long long, float, double, void, struct, union, enum
    等。
  • 在基本資料型別前加unsigned表示無符號型別,最高位不作為符號位,而是數值位。
  • 整數預設是int,小數預設是double。所以定義long型別時最好在數字末尾加l, L,定義float型別最好在數字末尾加f, F
基本資料型別 位元組數 範圍
char 1 -128~127
short 2 -32768~32767
int 2 或 4 -231~231-1
long 4 -231~231-1
long long 8 -264~264-1
float 4 --
double 8 --

變數的儲存型別

變數除了有資料型別,還有另外一個重要屬性,儲存型別。資料型別決定了變數的大小,而儲存型別決定了變數的生命週期和可見性。

C 語言中,變數的儲存型別有 4 種,auto, static, register, extern,一般使用的只有前兩個。

  • auto,預設儲存型別。auto 只能用在函式內,修飾區域性變數。
  • static,全域性變數預設以 static 修飾。當修飾區域性變數時,必須且只能被初始化一次
  • register,用於定義儲存在暫存器中而不是 RAM 中的區域性變數,用於運算要求高的變數。
  • extern,用於提供一個全域性變數的引用,全域性變數對所有的程式檔案都是可見的。

算數運算子

  • +, -, *, /, %, ++, --, <, <=, >, >=, ==, !=, &&, ||, !, &, |, ~, ^, >>, <<, +=, sizeof, ., [], *, (), ?:, 強制型別轉換
  • a^b=c, 則a^c=b, b^c=a,這一規律廣泛運用於密碼學。
  • a>>n,相當於a/(2^n)
    • 有符號數,右移時最高位補符號位。
    • 無符號數,右移時最高位補 0。
  • a<<n,相當於a*2^n,最低位直接補 0。

陣列與字串

  • int a[10]={1, 2, 3},前 4 個數分別為 1, 2, 3,後面的數均初始化為 0。
  • 字串的末尾必須是\0,佔一個位元組。

指標

變數都存放在從某個記憶體地址開始連續的若干個位元組中。

  • 開始的記憶體地址:指標。
  • 連續的若干位元組:指標對應型別的位元組長度。
  • int *p = (int *)1000,指標指向記憶體地址為 1000 的位置,管轄後面連續的 4 個位元組。

指標變數的大小

指標變數是一種資料型別,它的大小均為sizeof(T *) = 4,故無論指標變數指向的是何種資料型別,它的大小均為 4 位元組。

這是因為當今主流的 CPU 為 32 位,定址範圍是 2^32=4G,所以指標變數只需要 32 位即可。當然對於 64 位 CPU,編譯器產生的指標長度也就是sizeof(T *) = 8

主機位元組順序

  • 大端:低地址存放高位資料,這種最為直觀。
  • 小端:低地址存放低位資料,在程式和網路中,一般採用的是小端。

例如,將0x1234bacd以不同的方式儲存:

地址 大端 小端
0x0000 12 cd
0x0001 34 ba
0x0002 ab 34
0x0003 cd 12
unsigned short a = 0x1234;
char *p = (char *)&a;
for(int i=0;i<2;i++){
    printf("%2x\n", *(p+i));
}

如果short a = 0xf234,在輸出首位元組的0xf2時,會出現fffffff2而不是f2,猜測可能是由於printf()函式在輸出時將char自動轉換成了 4 位元組的int造成的,因為%x對應的就是整型,整型預設是int

指標運算

  • 同類型的指標可以比較。
  • 同類型的指標可以相減。p2-p1 = (p2-p1)/sizeof(T)
  • 指標可以和整型常量相加,相減。p ± n = p ± n*sizeof(T)
  • 指標可以自增,自減。

指標與動態分配記憶體

  • 分配記憶體:char *p = (char *)malloc(200 * sizeof(char))
  • 釋放記憶體:free(p)

指標型別

  • 空指標,也稱野指標,未指向任何地址的指標。p = NULL
  • void 指標,指向 void 型別的指標,可以強制轉換為任意型別指標。
  • 指向指標的指標,int **p
  • 陣列和字串與指標,指向陣列和字串首地址的指標。對於二維陣列,還區分行指標和列指標,行指標也就是指向指標的指標。
  • 指向函式的指標,該類指標指向某一型別的函式。
  • 常量指標,指標的指向可以改,但是指向的值不可以改。const int *p = &a
  • 指標常量,指標的指向不可以改,但是指向的值可以改。int * const p = &a
int add(int a, int b)
{
    return a+b;
}

int main()
{
    int (*p)(int, int); // 定義了一個指向某一型別的函式指標p
    p = add;
    cout << p(1, 2) << endl;
}

結構體

定義結構體

stuct student
{
    string name;
    int age;
};

使用結構體

// 初始化
student s1;
student s2 = {"Tom", 18};

cout << s2.name << endl; // 變數

student s3 = {
    name : "Alice",
    age : 16,
};
student *p = &s3;

cout << p->name << endl; // 指標

聯合體

定義聯合體

union data
{
    int a;
    float b;
    char c[20];
}

聯合體的大小以最大的型別為主,也就是說上面定義的data聯合體的大小為 20 位元組。

使用聯合體

聯合體在同一時間只使用一個變數,因為他們共用一塊最大的記憶體。

union data data1;

data1.i = 10;
data1.f = 220.5; // 此時會覆蓋掉資料 10
strcpy( data1.str, "C Programming"); // 此時會覆蓋掉資料 220.5

printf( "data.str : %s\n", data1.str);

列舉型別

// 第一個列舉成員的預設值為整型的 0,後續列舉成員的值在前一個成員上加 1
enum DAY
{
    SUN, MON, TUE, WED, THU, FRI, SAT
};

// 可以為單獨某個值指定數值,後續列舉成員的值仍在這個基礎上加 1
// 如下,spring = 0, summer = 3, autumn = 4, winter = 5
enum season {spring, summer=3, autumn, winter};

預處理

引用標頭檔案

  • #include <stdio.h>
  • #include "mymath.h"

巨集定義與條件編譯

#define, #undef, #ifdef, #ifndef, #if, #else, #elif, #endif

帶引數巨集

#define add(a, b) a+b

標頭檔案的多次引用

如果一個頭檔案被引用兩次,編譯器會處理兩次標頭檔案的內容,這將產生錯誤。為了防止這種情況,標準的做法是把檔案的整個內容放在條件編譯語句中,一般使用#idndef xx來解決這一問題,放在標頭檔案中。

#ifndef _MYMATH_H
#define _MYMATH_H
// xxx
#endif

extern "C"

extern "C"的主要作用就是為了能夠正確實現 C++ 程式碼呼叫其他 C 語言程式碼。加上extern "C"後,會指示編譯器這部分程式碼按 C 語言(而不是 C++)的方式進行編譯。

由於 C 和 C++ 編譯器(對應 gcc 和 g++)對函式的編譯處理是不完全相同的,尤其對於 C++ 來說,支援函式的過載,編譯後的函式一般是以函式名和形參型別來命名的。例如函式void fun(int, int),編譯後的可能是_fun_int_int(不同編譯器可能不同,但都採用了類似的機制,用函式名和引數型別來命名編譯後的函式名)。而 C 語言沒有類似的過載機制,一般是利用函式名來指明編譯後的函式名的,對應上面的函式可能會是_fun這樣的名字。

// 模組 A 標頭檔案 include/moduleA.h
#idndef _MODULE_A_H
#define _MODULE_A_H

int foo(int x, int y);

#endif

// 模組 A 實現檔案 include/moduleA.c
#include "moduleA.h"

int foo(int x, int y)
{
    return x + y;
}

// 主函式 src/main.cpp
#include <bits/stdc++.h>

#include "moduleA.h"

using namespace std;

int main()
{
    printf("%d\n", foo(1, 2));
    return 0;
}

此時,如果模組 A 的實現檔案採用 C 編譯,而模組 B 採用 C++ 編譯,那麼在連結階段,連結器會從模組 A 生成的目標檔案 moduleA.obj 中找_foo_int_int這樣的符號,顯然這是不可能找到的,因為foo()函式被編譯成了_foo的符號,因此會出現連結錯誤

gcc -c moduleA.c && cd ../src
g++ -I ../include main.cpp ../include/moduleA.o

# 報錯
/bin/ld: /tmp/cc6vh97T.o: in function `main':
main.cpp:(.text+0x13): undefined reference to `foo(int, int)'
collect2: error: ld returned 1 exit status

如果在主函式檔案中,加入extern "C",對引用標頭檔案部分指明用 C 語言編譯,即可正確呼叫。

#include <bits/stdc++.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "moduleA.h" // C 編譯

#ifdef __cplusplus
}
#endif

using namespace std;

int main()
{
    printf("%d\n", foo(1, 2));
    return 0;
}

這個功能十分有用處,因為在 C++ 出現以前,很多程式碼都是 C 語言寫的,而且很底層的庫也是 C 語言寫的,為了更好的支援原來的 C 程式碼和已經寫好的 C 語言庫,需要在 C++ 中儘可能的支援 C,而extern "C"就是其中的一個策略。這個功能主要用在下面的情況:

  • C++ 程式碼呼叫 C 語言程式碼
  • 在 C++ 的標頭檔案中使用
  • 在多個人協同開發時,可能有的人比較擅長 C 語言,而有的人擅長 C++,這樣的情況下也會有用到。
#ifndef __INCvxWorksh // 防止該標頭檔案被重複引用
#define __INCvxWorksh

#ifdef __cplusplus // 告訴編譯器,如果定義了 __cplusplus(即如果是cpp檔案),那麼這部分程式碼按 C 語言的格式進行編譯,而不是 C++
extern "C"{
#endif

// 這裡可以是複合語句
// 也可以是標頭檔案,相當於標頭檔案中的宣告都加了 extern "C"
/*...*/

#ifdef __cplusplus
}
#endif

/*...*/

#endif // end of __INCvxWorksh

讀寫操作

標準輸入輸出

  • printf(),標準輸出到stdout, stderr
  • scanf(),標準輸入到stdin
  • int fgetc(FILE *),返回輸入的一個字元,EOF時返回-1。
  • int fputc(int c, FILE *),輸出一個字元。
  • char *fgets(char *s, int n, FILE *),輸入一段文字以回車結束。
  • int fputs(const char *s, FILE *),輸出一段文字。
  • sscanf(buf, "%d%d", &a, &b),從陣列中讀。
  • sprintf(buf, "%d%d", a, b),寫入陣列。
  • fscanf(FILE *, 其他),從檔案讀。
  • fprintf(FILE *, 其他),寫入檔案。

重定向

  • freopen("in.txt", "r", stdin);
  • freopen("out.txt", "w", stdout);

檔案讀寫

開啟檔案

FILE *fopen(const char * filename, const char * mode);,其中,filename是檔案路徑的字串,mode是開啟模式,如下:

模式 含義
r 只讀
w 覆蓋寫,若檔案不存在則建立
a 追加寫,若檔案不存在則建立
xb 操作的是二進位制
x+ 允許讀寫
讀寫檔案
  • int fputc( int c, FILE *fp );,返回輸出的字元,否則 EOF。
  • int fputs( const char *s, FILE *fp );,成功返回一個非負值,否則 EOF。
  • int fgetc( FILE * fp );,返回讀取的字元,否則 EOF。
  • char *fgets( char *buf, int n, FILE *fp );
  • fscanf(FILE *, 其他),從檔案讀。
  • fprintf(FILE *, 其他),寫入檔案。
  • size_t fread(void *buf, size_t size_of_elements, size_t number_of_elements, FILE *a_file);,返回已讀取位元組數。
  • size_t fwrite(const void *buf, size_t size_of_elements, size_t number_of_elements, FILE *a_file);,返回已寫入位元組數。

  • int fseek(FILE *stream, long offset, int whence);,移動檔案指標到指定位置。
    • offset是相對於 whence 的偏移位置,正數往後,負數往前。
    • whence可以是SEEK_SET, SEEK_CUR, SEEK_END,分別是檔案頭,當前位置,檔案尾。
  • int ftell(FILE *stream),返回當前檔案指標位置距離檔案頭的位元組數。

通過上面兩個函式可以獲取一個檔案的大小:

FILE *fp = fopen("in.txt", "r");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);

關閉檔案

int fclose( FILE *fp );

時間函式

struct tm {
   int tm_sec;         /* 秒,範圍從 0 到 59 */
   int tm_min;         /* 分,範圍從 0 到 59 */
   int tm_hour;        /* 小時,範圍從 0 到 23 */
   int tm_mday;        /* 一月中的第幾天,範圍從 1 到 31 */
   int tm_mon;         /* 月,範圍從 0 到 11 */
   int tm_year;        /* 自 1900 年起的年數 */
   int tm_wday;        /* 一週中的第幾天,範圍從 0 到 6 */
   int tm_yday;        /* 一年中的第幾天,範圍從 0 到 365 */
   int tm_isdst;       /* 夏令時 */
};
  • time_t time(time_t *t),返回自元時間以來經過的秒數,該秒數會同時返回給引數和返回值。
  • struct tm *localtime(const time_t *t),返回當地時區時間。
  • struct tm *gmtime(const time_t *t),返回用標準格林尼治時區(GMT)表示。
  • time_t mktime(stuct tm *time),將結構體時間轉化為秒數。
  • double difftime(time_t t2, time_t t1),返回 t2-t1 的差值。
  • size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr),根據 format 中定義的格式化規則,格式化結構 timeptr 表示的時間,並把它儲存在 str 中。
  • clock_t clock(void),返回處理器時鐘所以用時間,用於得到程式使用時間。

其他

命令列引數

// ./a.ot hahaha
int main(int argc, char *argv[])
{
    printf("可執行程式 %s,引數個數為[%d],執行輸出:[%s]\n", argv[0], argc, argv[1]);
    return 0;
}

控制檯動態重新整理結果

printf("\r%d", count++);
// printf("\b\b%d", count++);

CPP 的基礎

定義常量

  • #define PI 3.141592654
  • const double PI = 3.1415926;

CPP 關鍵字

bool, namespace, using, class, private, public, protected, this, super, template, new, delete, inline, virtual, true, false

引用型別

引用變數是一個別名,引用是一種安全指標的機制。T & a = b;

int a = 5;
int &b = a; // b 為 a 的引用
int swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

int a = 1, b= 2;
swap(a, b);

C++ 指標與動態分配記憶體

C++ 程式中的記憶體分為兩個部分:

  • 棧:在函式內部宣告的所有變數都將佔用棧記憶體。
  • 堆:這是程式中未使用的記憶體,在程式執行時可用於動態分配記憶體。

很多時候,我們無法提前預知需要多少記憶體來儲存某個定義變數中的特定資訊,所需記憶體的大小需要在執行時才能確定。在 C++ 中,就可以使用 new 運算子為給定型別的變數在執行時分配堆內的記憶體,它會返回所分配的空間地址。

  • 分配記憶體:int *p = new int, int *p = new int[10]
  • 釋放記憶體:delete p, delete[] p

形參的預設值

在 CPP 中,函式的形參可以指定預設值,並且擁有預設值的引數必須放在最後面。

若定義的函式有前向引用申明,那麼預設引數值必須在宣告中定義,在實現的時候不可以再次指定預設值。

函式的過載

函式名相同,函式的引數個數或者引數型別不同,則稱之為函式過載。

函式的返回值型別不可以作為判斷函式過載的依據。

行內函數

定義函式時在返回型別前面新增關鍵字inline,在編譯時會用在呼叫函式的位置展開函式體,以減少引數傳遞,控制轉移等的開銷。

行內函數體內不能有迴圈和 switch 等語句,函式體必須簡單。

異常處理

try {
    // 保護程式碼
} catch (ExceptionName e1) {
    // catch 塊
    cout << e1.what() << endl;
} catch (ExceptionName e2) {
    // catch 塊
} catch (ExceptionName eN) {
    // catch 塊
} catch (...) {
    // 處理任何異常
}
try {
    if (str == "int") throw 100;
    else throw "ahhaha";
} catch(const int a) {
    cout << a << endl;
} catch(const string b) {
    cout << b << endl;
} catch( ... ) {
    cout << "other exception" << endl;
}

多執行緒

#include <iostream>
#include <thread>

using namespace std;

void fun(int num)
{
    while(true){
        cout << num << endl;
    }
}

int main()
{
    thread t1(fun, 1);
    thread t2(fun, 2);

    // t1.join(),主執行緒會阻塞在這裡
    // t1.detach(),執行緒會後臺執行,無需阻塞等待
    // 主執行緒不會等待子執行緒結束
    t1.join();
    t2.join();
}