exiler

exiler

C++ 入門-1

基本內置類型#

不同的編譯器和平台可能會有差異,下表為在 Windows_X64 環境下使用 MinGw GCC 9.2.0 32_bit 為例:

類型含義最小尺寸範圍
bool布爾類型8bitstrue 或 false
char字符型8bits-2^7 ~ 2^7-1
wchar_t寬字符16bits0 ~ 2^16 - 1
char16_tUnicode 字符16bits0 ~ 2^16 - 1
char32_tUnicode 字符32bits0 ~ 2^32 - 1
short短整型16bits-2^15 ~ 2^15-1
int整型32bits-2^31 ~ 2^31-1
long長整型32bits-2^31 ~ 2^31-1
long long長整型64bits-2^63 ~ 2^63-1
float單精度浮點型32bits大約 7 位有效數字
double雙精度浮點型64bits大約 15 位有效數字
long double擴展精度浮點型96bits更高的有效數字

可以在 IDE 中編譯運行以下代碼查看本機算數類型的尺寸

// Windows_X64環境下MinGw GCC 9.2.0 32_bit為例
// 1Byte = 8bit
#include<iostream>
using namespace std;
int main(){
  cout<< "int * "<<sizeof(int*)*8<<endl; // 32 bits
  cout<< "bool "<<sizeof(bool)*8<<endl;  // 8 bits
  cout<< "char "<<sizeof(char)*8<<endl;  // 8bits
  cout<< "wchar_t "<<sizeof(wchar_t)*8<<endl;  // 16bits
  cout<< "char16_t "<<sizeof(char16_t )*8<<endl;  // 16bits
  cout<< "char32_t "<<sizeof(char32_t )*8<<endl;  // 32bits
  cout<< "short "<<sizeof(short )*8<<endl;  // 16bits
  cout<< "int "<<sizeof(int )*8<<endl;  // 32bits
  cout<< "long "<<sizeof(long )*8<<endl;  // 32bits
  cout<< "long long"<<sizeof(long long)*8<<endl;  // 64bits
  cout<< "float "<<sizeof(float)*8<<endl;  // 32bits
  cout<< "double "<<sizeof(double)*8<<endl; // 64bits
  cout<< "long double "<<sizeof(long double)*8<<endl; //96bits
return 0;
}

內置類型的機器實現#

字節 (Byte) 是可尋址的最小內存塊,1 byte = 8 bit (比特)

字 (Word) 是存儲的基本單元,1 Word = 4 字節或 8 字節

內存中每個字節與一個數字(被稱為地址 address)關聯,我們能夠使用某個地址來表示從這個地址開始的大小不同的比特串,如地址為 736424 的那個字或者地址 736424 的那個字節。但是必須知道存儲在某地址的數據類型,才能賦予內存該地址明確含義,類型決定數據所占比特數,以及如何解釋這些比特的內容。

帶符號型和無符號型#

其它整型

  • 除去布爾型和擴展字符型外,其它整型可劃分為帶符號的和無符號的

  • int, short, long, long long 帶符號

  • 類型名前添加unsigned 得到無符號類型

  • unsigned long

  • unsigned int 可簡寫為 unsigned)

字符型

  • 分三種 char, signed char, unsigned char

  • char 表現為帶符號還是無符號的,由編譯器決定

如何選擇類型#

  1. 數值不可能是負數時,比如年齡,長度等等,選用無符號類型
  2. 整數計算式使用int類型,一般long的大小和int一樣,而short顯得太小。如果處理較大的整數,應該選擇long long
  3. 算數表達式中不要使用boolchar,符號容易出問題;
  4. 浮點數據運算使用double

類型轉換#

類型所能表示值的範圍決定轉換過程:

  • 非布爾型賦給布爾型,初始值為 0 則結果為 false, 否則為 true。
#include<iostream>
using namespace std;
int main() {
	int i = 42;
	if (i) {
		i = 0;
	}
	cout << "i = " << i << endl;  // i=0
	return 0;
}
  • 布爾型賦給非布爾型,初始值為 false 結果為 0,初始值為 true 結果為 1。
  • 浮點數賦給整型,近似處理,保留小數點前的部分。
  • 整型賦給浮點型,小數部分記 0,若整型超過浮點型容量則精度可能有損失。
  • 超出範圍值賦給無符號數,結果是初始值對無符號類型表示的最大值取模余數。
  • 超出範圍值賦給有符號數,結果未定義。
//test01.cpp
#include<iostream>
using namespace std;
int main() {
	int i = 42;
	if (i) {
		i = 0;
	}
	cout << "i = " << i << endl;  // i=0
	return 0;
}
//test02.cpp
#include<iostream>
int main() {
	unsigned u = 10;
	int i = -42;
	std::cout << i + i << std::endl; //-84
	std::cout << u + i << std::endl; //4294967264
	return 0;
}
//test03.cpp
#include<iostream>
int main() {
	for (int i = 10; i >= 0; --i) {
		std::cout << i << std::endl;
	}
	return 0;
} //正常輸出
//test04.cpp
#include<iostream>
int main() {
	for (unsigned u = 10; u >= 0; --u) {   //無符號數不會小於0
		std::cout << u << std::endl;    //死循環,ctrl+c退出
	}
	return 0;
}
//test05.cpp
#include<iostream>
int main() {
	unsigned u = 10, u2 = 42;
	std::cout << u2 - u << std::endl;  //32
	std::cout << u - u2 << std::endl;  //4294967264
	int i = 10, i2 = 42;
	std::cout << i2 - i << std::endl;  //32
	std::cout << i - i2 << std::endl;  //-32
	std::cout << i - u << std::endl;  //0
	std::cout << u - i << std::endl; //0
	return 0;
}

字面值常量 (Literal Constants)#

  1. 整型字面值常量:
  • 十進制 :42
  • 八進制: 052 (以 0 開頭)
  • 十六進制: 0x2A 或 0X2A (以 0x 或 0X 開頭)
  • 二進制: 0b1010100B101010(以 0b 或 0B 開頭)

錯誤示範:

#include<iostream>
int main() {
	int month = 9, day = 7;
	int month1 = 09, day1 = 07; //以0開頭會當成8進制賦值
    // [Error] invalid digit "9" in octal constant
	return 0;
}
  1. 浮點型字面值常量:
  • 十進制:3.142.0
  • 科學計數法:3.0e8(表示 3.0 乘以 10 的 8 次方)
  1. 字符型字面值常量:
  • 字符:'A''x'
  • 轉義字符:'\n'(表示換行符)或 '\\'(表示反斜杠)
轉義序列:
換行'\n'   橫向制表符'\t'   響鈴'\a'
縱向制表符'\v'  退格符'\b'  雙引號'\"'
反斜線'\\'  問號'\?'  單引號'\''   回車'\r' 
泛化轉義序列:'\x'跟1個多個16進制數,或'\'後跟1、2、3個8進制數
'\7'響鈴 '\12'換行 '\40'空格
'\0'空字符 '\115'字符M '\x4d'字符M
//test05.cpp
#include<iostream>
int main() {
	std::cout << '\n';  //換行
	std::cout << "\tHi!\n";  //制表符,Hi!,換行
	std::cout << "\v\?\abc\b\n";  //縱向制表符,?,響鈴,bc,退格,換行
	std::cout << "Hi \x4dO\115!\n";  //Hi MOM!,換行
	std::cout << '\115' << '\n';  //M,換行
	return 0;
}
  1. 字符串字面值常量:字符串字面值常量表示一串字符,使用雙引號括起來。例如:
  • 字符串:"Hello, World!"

字符串實際類型是字符數組,結尾添加 '\0' 字符 字符串型實際上是常量字符構成的數組,結尾處以 '\0' 結束,所以字符串類型實際上長度比內容多 1。

  1. 布爾字面值常量:布爾字面值常量表示真或假,可以是 truefalse

  2. 指針字面值: nullptr

  3. 指定字面值類型:

字符和字符串字面值:

前綴含義類型
uUnicode16 字符char16_t
UUnicode32 字符char32_t
L寬字符wchar_t
u8UTF-8 (字符串字面常量)char

整型字面值:

後綴最小匹配類型
u or Uunsigned
l or Llong
ll or LLlong long

浮點型字面值:

後綴類型
f or Ffloat
l or Llong double

變量#

變量提供一個具名的、可供程序操作的存儲空間。 C++ 中變量和對象一般可以互換使用

define#

  • 定義形式:類型說明符(type specifier) + 一個或多個變量名組成的列表;

  • 初始化(initialize):對象在創建時獲得了一個特定的值。

    • 初始化不是賦值 ,初始化 = 創建變量 + 賦予初始值 ,賦值 = 擦除對象的當前值 + 用新值代替
  • 列表初始化:使用花括號 {}

  • 若列表初始化且初始值存在丟失信息的風險,則編譯器將報錯。

    int a{3.14};
    
    // error: narrowing conversion from ‘double’ to ‘int’ 
    
  • 默認初始化:定義時沒有指定初始值會被默認初始化;

  • 在函數體內部的內置類型變量將不會被初始化。

  • 類的對象如果沒有顯示初始化,其值由類定義。

#include<iostream>
std::string global_str;  //定義全局變量global_str,初值為空字符串
int global_int;  //定義全局變量global_int,初值為0
int main() {
	int local_int; //局部變量未初始化,初值未定義
	std::string local_str; 
    //local_str 是 string類的對象,它的值由類確定,為空字符串。
	return 0;
}

建議初始化每一個內置類型變量!

變量聲明與定義的聯系#

把程序拆分成多個邏輯部分來編寫 ,C++ 支持分離式編譯機制,程序分割為若干文件,每個文件可被獨立編譯,為了支持分離式編譯,C++ 將聲明和定義區分開。

聲明使得名字為程序所知,定義負責創建與名字關聯的實體。

聲明規定了變量類型和名字。定義申請存儲空間,也可能為變量賦初值。想聲明一個變量而非定義它,在變量名前加關鍵字extern, 不要顯示初始化變量。

變量只能被定義一次,但是可以多次聲明,定義只出現在一個文件中,其他文件使用該變量時需要對其聲明。

extern int i;  //聲明i而非定義 
int j;  //聲明並定義j 
// 包含了顯示初始化的聲明,就變成了定義:
extern double pi = 3.14;  //定義
// 函數體內部,試圖初始化一個extern關鍵字標記的變量,將引發錯誤。
extern int ix = 1024; //定義
int iy;  //定義
extern int iz;  //聲明

變量標識符與命名規範#

標識符:

  1. 由字母、數字、下劃線組成,必須以字母或下劃線開頭。
  • 長度沒有限制。

  • 大小寫寫敏感。

  1. 保留名字不能用作標識符。

  2. 用戶自定義標識符

  • 不能連續出現兩個下劃線
  • 不能以下劃線緊跟大寫字母開頭
  • 定義在函數體外的標識符不能以下劃線開頭
  • 使用小駝峰命名法
//c++關鍵字
alignas  continue  friend  register  true
alignof  decltype  goto  reinterpret_cast  try
asm  default  if  return  typedef
auto  delete  inline  short  typeid
bool  do  int  signed  typename
break  double  long  sizeof  union
case  dynamic_cast  mutable  static  unsigned
catch  else  namespace  static_assert  using
char  enum  new  static_cast  virtual
char16_t explicit  noexcept  struct  void
char16_t export  nullptr  switch  volatile
class  extern  operator  template  wchar_t
const  false  private  this  while
constexpr  float  protected  thread_local
const_cast  for  public  throw
//c++操作符替代名
and  bitand  compl  not_eq  or_eq  xor_eq
and_eq  bitor  not  or  xor

命名規範:

  1. 體現實際含義
  2. 變量名和函數名使用小駝峰命名法,如myVariable
  3. 自定義類名大寫字母開頭,如Sales_item
  4. 若標識符有多個單詞組成,單詞之間要有明顯區分,如student_loan,studentLoan, 不要用studentloan
 int double = 3.14;  //error
 int _;
 int catch-22;  //error
 int 1_or_2 = 1;  //error
 double Double = 3.14;

名字的作用域(namescope),以 {} 分隔

  • 全局作用域
  • 塊作用域

第一次使用變量時再定義它,更容易找到,賦予比較合理的初始值。

//scope.cpp
#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same
int reused = 42; // reused has global scope
int main() {
	int unique = 0; // unique has block scope
// output #1: uses global reused; prints 42 0
	std::cout << reused << " " << unique << std::endl;
	int reused = 0; // new, local object named reused hides global reused
// output #2: uses local reused; prints 0 0
	std::cout << reused << " " << unique << std::endl; 
    //此時全局變量被同名的局部變量覆蓋
// output #3: explicitly requests the global reused; prints 42 0
	std::cout << ::reused << " " << unique << std::endl;  
    //使用全局變量
	return 0;
}

嵌套的作用域:

同時存在全局和局部變量時,已定義局部變量的作用域中可用::顯式訪問全局變量。用到全局變量時,盡量不使用重名的局部變量。

#include <iostream>
int i = 42;
int main() {
	int i = 100;
	int j = i;
//j的值是100,局部變量i覆蓋了全局變量i
	std::cout << j << std::endl; //100
	return 0;
}
#include <iostream>
int main() {
//下面的程序是否合法?如果合法,它將輸出什麼?
	int i = 100, sum = 0;
	for (int i = 0; i != 10; ++i)
		sum += i;  //此時的變量i僅在for循環內生效
	std::cout << i << " " << sum << std::endl;
//合法輸出:100 45 
	return 0;
}

左值和右值#

  • 左值(l-value)可以出現在賦值語句的左邊或者右邊,比如變量;

  • 右值(r-value)只能出現在賦值語句的右邊,比如常量。

引用#

一般說的引用是指的左值引用

引用:引用是為對象起了另外一個名字,引用類型:引用(refer to)另外一種類型。

 int &refVal = val; 
  1. 引用必須初始化:
 int &refVal2;  //報錯,引用必須被初始化 
  1. 引用和它的初始值是綁定 bind在一起的,而不是拷貝。一旦定義就不能更改綁定為其他的對象
  2. 引用類型要與綁定對象匹配
  3. 引用只能綁定在對象上,不能與字面值或表達式計算結果綁定

更詳細的解釋可參考:C++ 中的引用

指針#

是一種 "指向(point to)" 另外一種類型的復合類型。 本身就是一個對象,無需定義時賦值

定義指針類型#

int *ip1,*ip2;
//ip1和ip2都是int型對象指針
double dp,*dp2
//dp2 是指向double型對象的指針,dp是double類型

獲取對象的地址#

指針存放某個對象的地址。 獲取對象的地址:

int i=42;
int *p = &i; 

&是取地址符。 指針的類型與所指向的對象類型必須一致(均為同一類型 int、double 等)

int ival = 42;
int *p = &ival; //p存放ival的地址,p是指向val的指針

double dval;
double *pd = &dval;//正確
double *pd2 = pd;//正確

int *pi = pd;//錯誤,類型不匹配
pi = &dval;//錯誤,試圖把double對象的地址賦給int指針

指針的值 (即地址) 的四種狀態:

  1. 指向一個對象;
  2. 指向緊鄰對象的下一個位置;
  3. 空指針;
  4. 無效指針。

對無效指針的操作均會引發錯誤;

第二種和第三種雖為有效的,訪問指針對象的行為後果無法預計。

指針訪問對象#

#include <iostream>
int main() {
//如果指針指向一個對象,使用解引用符(操作符'*')來訪問對象
	int ival = 42;
	int *p = &ival;
	std::cout << *p;// 輸出p指針所指對象的數據,
	*p = 0;
	std::cout << ival; // 0
	return 0;
}

解引用操作僅適用於確實指向某個對象的有效指針。

空指針#

int *p1=nullptr; //使用空指針。
int *p2 = 0;//p2 初始化為字面常量0
// include<cstdlib>
int *p3 = NULL;//int *p3=0 NULL預處理變量
int zero = 0;
p1 = zero; //錯誤,不能把int變量直接賦給指針

建議初始化所有指針!

指針與引用的區別#

  • 引用本身並非一個對象,引用定義後就不能綁定到其他的對象上;
  • 指針並沒有此限制
//說明指針和引用的主要區別
    
//引用是另一個對象的別名,而指針本身就是一個對象。
//引用必須初始化,並且一旦定義了引用就無法再綁定到其他對象。
//而指針無須在定義時賦初值,也可以重新賦值讓其指向其他對象。
int i = 42;
int *pi = 0;//pi 初始化,沒有指向任何對象
int *pi2 = &i;//pi2指向i
int *pi3;//如果pi3定於於塊內,pi3值無法確定

pi3 = pi2;//pi3與pi2指向同一對象i
pi2 = 0;//pi2 不指向任何對象

//賦值語句永遠改變的是左側的對象。
pi = &ival;//pi指向了ival
*pi = 0;//ival值改變,pi沒改變
//請解釋下述定義。在這些定義中有非法的吗?如果有,為什麼?
int i = 0;
(a) double* dp = &i;
(b) int *ip = i;
(c) int *p = &i;
(a): 非法。不能將一個指向 double 的指針指向 int
(b): 非法。不能將 int 變量賦給指針。
(c): 合法。

其他指針操作#

int ival = 1024;
int *p1=0;
int *p2=&ival;
//指針值0,條件取false
if(p1)
	//...
//p2指針非0,條件值是true
if(p2)
	//...
//指針比較
if(p1==p2)
	//...

非法指針會引發不可預計的後果!

//一條語句定義不同類型變量
int i=1024,
*p=&i,&r=i;

int* p;//合法但容易產生誤導
int* p1,p2;//p1是指針,p2是int
int *p1,*p2;//p1,p2都是指針

int *p1;
int *p2;

指向指針的指針#

#include <iostream>
using namespace std;
int main() {
	int ival = 1024;
	int *pi = &ival;  //pi指向int型
	int **ppi = &pi; //ppi指向int型指針
// 解引用
	cout << "The value of ival\n"  
	     << "direct value: " << ival << "\n"
	     << "indirect value: " << *pi << "\n"
	     << "doubly indirect value: " << **ppi
	     << endl;
	return 0;
	//The value of ival
	//direct value: 1024
	//indirect value: 1024
	//doubly indirect value: 1024
}

指向指針的引用#

從右向左閱讀 r 的定義

int i=42;
int *p;
int *&r = p;  //r是對指針p的引用
r = &i;  //p指向i
*r=0;  //i=0
說明下列變量的類型和值。
(a) int* ip, i, &r = i;
(b) int i,*ip = 0;
(c) int* ip, ip2;

(a): ip 是一個指向 int 的指針, i 是一個 int, r 是 i 的引用。
(b): i 是 int , ip 是一個空指針。
(c): ip 是一個指向 int 的指針, ip2 是一個 int

void 指針#

void指針可以存放任意對象的地址。因無類型,僅操作內存空間,不能直接操作void * 指針所指的對象。

double obj = 3.14,
*pd = &obj;
void *pv = &obj; //obj 可以是任意類型的對象
pv = pd;//void* 可存任何類型,使用時再進行強制類型轉換即可

CONST 限定符#

const:定義一些不能被改變值的變量, const 對象一旦創建值不再改變,所以必須初始化,且不能被改變。

const int bufSize = 512;
bufSize = 512;//error,試圖寫值到const對象

const int i= getSize();
const int j=42;
const int k;//error,k未初始化常量

int i=42;
const int ci=i;
int j=ci;

CONST 對象僅在文件內有效:

  • 當多個文件出現同名的 const 變量,等同於在不同文件分別定義獨立變量。

  • 要想在多個文件中使用 const 變量共享,定義和聲明都加 extern 關鍵字即可。

//file_1.cc定義初始化常量,能被其它文件訪問
extern const int bufSize = fcn();

//file_1.h 頭文件
extern const int bufSize ;//與file_1.cc中定義的bufSize是同一個

const int buf;  // 不合法, const 對象必須初始化
int cnt = 0;  // 合法
const int sz = cnt;  // 合法
++cnt; ++sz;  // 不合法, const 對象不能被改變

const 的引用#

  • 把引用綁定到 const 對象上,稱之為對常量的引用。
  • 與普通引用不同,對常量的引用不能被用作修改它所綁定的對象
const int ci=1024;
const int &r1=ci;
r1=42;  //error r1是對常量的引用
int &r2 = ci;  //error,試圖讓非常量引用指向一個常量對象

初始化和對 const 的引用#

引用類型必須與其所用對象類型一致

  • 例外:初始化常量引用時,允許用任意表達式做初始值,只要表達式的結果能轉換成引用的類型。
  • 允許為一個常量引用綁定非常量的對象,字面值,一般表達式
int i=42;
const int &r1=i;  //允許將const int&綁定到int對象
const int &r2=42;//r2 是個常量引用
const int &r3=r1*2;//r3是個常量引用
int &r4=r1*2;//error,r4是一个普通的非常量引用。

//當一個常量引用被綁定到另外一種類型發生了什麼?
double dval = 3.14;
const int &ri=dval;
//編譯器將上述代碼改為如下形式
const int temp=dval;//生成臨時整型變量
const int &ri=temp;//讓ri綁定這個臨時變量
  • 臨時量 (temporary) 對象:當編譯器需要一個空間來暫存表達式結果的值時,臨時創建的一個未命名的變量
  • 對臨時量的引用是錯誤行為:
double dval = 3.14;
int &ri = dval; //error

對 const 的引用很可能引用一個非 const 對象

  • 常量引用僅對引用可參與的操作做出限定,對引用的對象本身是不是常量未做限定
int i = 42;
int &r1 = i; //r1綁定i
const int &r2 = i;  //r2綁定i,不允許通過r2修改i
r1 = 0;  //ok,r1非常量
r2 = 0;  //error,r2是一個常量引用

和常量引用一樣,指向常量的指針同理也沒有規定所指對象必須是常量。僅要求不能通過指針改變對象值。

const 指針#

常量指針必須初始化,允許把指針定義成常量:

int errNumb=0;
int *const curErr=&errNumb;// curErr是常量,一直指向errNumb(地址不能變)
const double pi=3.1415;
const double *const pip= &pi;//pip是常量,*pip也是常量,
//pip 是指向常量對象的常量指針 (值與地址均不能更改)
*pip = 2.72;//error *pip 是常量
if(*curErr){
	errorHandler();
	*curErr=0;//ok *curErr不是常量
}
//下面的哪些初始化是合法的?請說明原因。
int i = -1, &r = 0; // 不合法, r 必須引用一個對象
int *const p2 = &i2; // 合法,常量指針
const int i = -1, &r = 0(常量引用); // 合法
const int *const p3 = &i2; // 合法
const int *p1 = &i2; // 合法
const int &const r2; // 不合法, r2 是引用,沒有頂層const
const int i2 = i, &r = i; // 合法
//說明下面的這些定義是什么意思,挑出其中不合法的。
int i,*const cp; // 不合法, const 指針必須初始化
int *p1,*const p2; // 不合法, const 指針必須初始化
const int ic, &r = ic; // 不合法, const int 必須初始化
const int *const p3; // 不合法, const 指針必須初始化
const int *p; // 合法. 一個指針,指向 const int
//假設已有上一個練習中定義的那些變量,則下面的哪些語句是合法的?請說明原因。
i = ic; // 合法, 常量賦值給普通變量
p1 = p3; // 不合法, p3 是const指針,不能賦值給普通指針
p1 = &ic; // 不合法, 普通指針不能指向常量

const int * const p3 = &ic; // 合法, p3 是常量指針且指向常量
p3 = &ic; //此時不合法 p3 常量

int * const p2 = p1; // 合法, 可以將普通指針賦值給常量指針
p2 = p1;//此時不合法 p2是常量

const int ic = *p3; // 合法, 對 p3 取值後是一個 int 然後賦值給 ic
ic = *p3;//此時不合法,ic是常量

頂層 const#

  • 頂層 const:指針本身是個常量。
  • 底層 const:指針指向的對象是個常量。拷貝時嚴格要求相同的底層 const 資格。
int i=0;
int *const p1=&i;//p1常量,頂層const
const int ci=42;//ci常量,頂層const
const int *p2=&ci;//*p2常量,底層const
const int *const p3=p2;//靠右頂層,靠左底層const
const int &r =ci;//用於聲明引用的const都是底層const

//拷貝時,頂層const不受影響
i=ci;//拷貝ci的值,ci頂層const,無影響
p2=p3;//p2 p3所指對象類型相同 p3頂層const部分不影響

//拷貝時嚴格要求相同的底層const資格,數據類型能夠轉換。
int *p =p3;//error p3包含底層const定義,p沒有
p2=p3;//ok p2 p3都是底層const
p2=&i; //ok int* 轉const int*
int &r=ci;//error 普通int& 不能綁定到int 常量
const int &r2=i;//ok const int& 可以綁定到int
//對於下面的這些語句,請說明對象被聲明成了頂層const還是底層const?
const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2,
*const p3 = &i, &r2 = v2;
//v2 是頂層const,p2 是底層const,p3 既是頂層const又是底層const,r2 是底層const。
//假設已有上一個練習中所做的那些聲明,則下面的哪些語句是合法的?
//請說明頂層const和底層const在每個例子中有何體現。
r1 = v2; // 合法, 頂層const在拷貝時不受影響
p1 = p2; // 不合法, p2 是底層const,如果要拷貝必須要求 p1 也是底層const
p2 = p1; // 合法, int* 可以轉換成const int*
p1 = p3; // 不合法, p3 是一個底層const,p1 不是
p2 = p3; // 合法, p2 和 p3 都是底層const,拷貝時忽略掉頂層const

constexpr 和常量表達式#

  • 常量表達式:指值不會改變,且在編譯過程中就能得到計算結果的表達式。
//一個對象(表達式)是不是常量表達式由數據類型和初始值共同決定
const int max_file=30;  //max_file是常量表達式
const int limit = max_file+1;  //limit是常量表達式
int staff_size = 7;  //staff_size不是常量表達式
const int sz = get_size();  //運行時獲取,sz是常量表達式。
  • C++11 新標準規定,允許將變量聲明為constexpr 類型以便由編譯器來驗證變量的值是否是一個常量的表達式。
constexpr int mf = 20;  //20是常量表達式
constexpr int limit = mf+1;  //mf+1是常量表達式
constexpr int sz = get_size(); //取決於get_size()是否是constexpr函數

字面值類型#

常量表達式的值編譯時就得到計算,類型簡單,值容易得到,稱為為 “字面值類型”(literal type)

  • 算數類型
  • 引用,指針,nullptr,0 或固定位置

指針和 constexpr#

  • constexpr 把所定義的對象置為頂層
const int *p=nullptr;//p是一個指向常量的指針
constexpr int *q=nullptr;//q是常量指針
//constexpr把所定義的對象置為頂層
constexpr int *nq=nullptr;//nq是常量指針
int j=0;
constexpr int i=42;//i是常量
//i,j定義在函數外
constexpr const int *p=&i;//p是常量指針,指向整型常量i
constexpr int *p1=&j;//p1是常量指針,指向整數j

處理類型#

  • 程序越來越複雜,類型越來越複雜。類型難於拼寫,明確目的含義,搞不清需要什麼類型。

類型別名#

  • 傳統別名:使用typedef來定義類型的同義詞。

    typedef double wages; 
    wages hour,weekly;  //double hour,weekly;
    
  • 新標準別名:別名聲明(alias declaration):

    using SI = Sales_item;  //(C++11)
    SI items;//Sales_item items;
    

指針,常量和類型別名#

// 對於復合類型(指針等)不能代回原式來進行理解
typedef char *pstring;  // pstring是char*的別名
const pstring cstr = 0; // cstr是指向char的常量指針
const pstring *ps;//ps是一個指針,對象是指向char的常量指針
//基本數據類型是指針而不是const char。

AUTO 類型說明符 (C++ 11)#

清楚的知道表達式類型並不容易,因此引入auto

  • auto 類型說明符:讓編譯器自行推斷類型。顯然 auto 定義變量必須要有初始值。
  • 一條生命一句只能有一種基本數據類型:
 auto sz = 0, pi =3.14//錯誤,類型不一致

復合類型,常量和 auto#

int i = 0, &r = i; 
auto a = r; //推斷a類型是int

//會忽略頂層const
const int ci = i;
auto b = ci;//推斷int,忽略頂層const

const int ci = 1; const auto f = ci;
//推斷類型是int,如果希望是頂層const需要自己加const

DECLTYPE 類型提示符 (C++ 11)#

decltype 是一個 C++11 中引入的關鍵字,用於獲取表達式的類型。它的語法如下:

decltype(expression)

其中,expression 是一個表達式,可以是變量、函數調用、類型轉換等。

decltype 的作用是根據表達式的類型推斷出一個變量或表達式的類型,並且返回該類型。例如:

int x = 42;
decltype(x) y; // y 的類型為 int

double square(double value) {
    return value * value;
}

decltype(square(3.14)) result; // result 的類型為 double

struct Point {
    int x;
    int y;
};

Point p;
decltype(p.x) a; // a 的類型為 int

在上面的示例中,decltype(x) 返回 int 類型,因為表達式 x 的類型是 intdecltype(square(3.14)) 返回 double 類型,因為表達式 square(3.14) 的返回類型是 doubledecltype(p.x) 返回 int 類型,因為 p.xPoint 結構體的一個成員變量。

decltype 在編寫泛型代碼時非常有用,可以根據參數的類型來推斷出函數的返回類型,避免手動指定返回類型。例如:

template<typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
    return a * b;
}

int main() {
    int x = 5;
    double y = 3.14;
    auto result = multiply(x, y); // result 的類型為 double

    return 0;
}

在上面的示例中,multiply 函數使用 decltype(a * b) 來推斷返回類型,根據傳入的參數類型,返回類型會自動推斷為 double

decltype 是一個非常有用的關鍵字,可以讓編譯器根據表達式的類型來推斷出變量或函數的類型,提高代碼的靈活性和可讀性。

  • 從表達式的類型推斷出要定義的變量的類型。

  • decltype:選擇並返回操作數的數據類型,不計算表達式的值。

decltype(f()) sum = x; //推斷sum的類型是函數f的返回類型。
  • 不會忽略頂層 const。
  • 如果對變量加括號,編譯器會將其認為是一個表達式,decltype((i))得到結果為引用。
  • 賦值是會產生引用的一類典型表達式,引用的類型就是左值的類型。也就是說,如果 i 是 int,則表達式 i=x 的類型是 int&。

自定義數據結構 STRUCT#

儘量不要把類定義和對象定義放在一起。如 struct Student {} xiaoming,xiaofang;

  • 類可以以關鍵字 struct 開始,緊跟類名和類體。
  • 類數據成員:類體定義類的成員。
  • C++11:可以為類數據成員提供一個類內初始值 (in-class initializer)。

下面舉例:

  • 定義 Sales_data 類:
//Sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H

#include <string>
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif
  • 使用 sales_data 類
//Sales_data.cpp
#include <iostream>
#include <string>
#include "Sales_data.h"
int main() {
	Sales_data data1, data2;
// code to read into data1 and data2
	double price = 0; // price per book, used to calculate total revenue
// read the first transactions: ISBN, number of books sold, price per b
	std::cin >> data1.bookNo >> data1.units_sold >> price;
// calculate total revenue from price and units_sold
	data1.revenue = data1.units_sold * price;
// read the second transaction
	std::cin >> data2.bookNo >> data2.units_sold >> price;
	data2.revenue = data2.units_sold * price;
// code to check whether data1 and data2 have the same ISBN
// and if so print the sum of data1 and data2
	if (data1.bookNo == data2.bookNo) {
		unsigned totalCnt = data1.units_sold + data2.units_sold;
		double totalRevenue = data1.revenue + data2.revenue;
// print: ISBN, total sold, total revenue, average price per book
		std::cout << data1.bookNo << " " << totalCnt
		          << " " << totalRevenue << " ";
		if (totalCnt != 0)
			std::cout << totalRevenue / totalCnt << std::endl;
		else
			std::cout << "(no sales)" << std::endl;
		return 0; // indicate success
	} else { // transactions weren't for the same ISBN
		std::cerr << "Data must refer to the same ISBN"
		          << std::endl;
		return -1; // indicate failure
	}
}

編寫自己的頭文件#

  • 頭文件通常包含一些只能被定義一次的實體:類、 const 和 constexpr 變量。

預處理器概述:

  • 預處理器(preprocessor):確保頭文件多次包含仍能安全工作。
  • 當預處理器看到 #include 標記時,會用指定的頭文件內容代替 #include
  • 頭文件保護符(header guard):頭文件保護符依賴於預處理變量的狀態:已定義和未定義。
    • #indef 已定義時為真
    • #inndef 未定義時為真
    • 頭文件保護符的名稱需要唯一,且保持全部大寫。養成良好習慣,不論是否該頭文件被包含,都要加保護符。
#ifndef SALES_DATA_H //SALES_DATA_H未定義時為真
#define SALES_DATA_H
strct Sale_data{
	...
}
#endif
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。