1. 程式人生 > 實用技巧 >c++-物件和類-基礎

c++-物件和類-基礎

ch4—物件和類(基礎)

本單元內容,主要是宣告類、用類定義物件,以及初步使用物件編寫程式的方法。

本單元中最容易讓人迷惑的就是物件的初始化、物件內部的資料成員的初始化問題。

本單元中難以理解的概念是 this 指標。這個特殊的指標,很難簡單地用文字描述清楚它的含義。想要理解this指標,一定要看相關的程式碼,把this放到程式碼中理解。

本單元介紹了兩個C++的類,分別是string和array

在C++中,我們不應該再使用C風格的字串,也不應再使用字元陣列儲存字串,而應改為使用 string 物件

C++11 在標準庫中添加了 array 這個類。今後我們在寫C++程式碼時,除非有明確的理由,否則我們都應該使用 array 類

,而避免使用 C風格的原生陣列。

1、類的概念

OOP(Object-Oriented Programming):利用物件進行程式設計

An object represents an entity in the real world that can be distinctly identified(物件表示現實世界中一個獨一無二的實體)

面向物件的特徵

  • 抽象 Abstraction

  • 多型 polymorphism

  • 繼承 inheritance

  • 封裝 encapsulation

物件由什麼構成?

  • 唯一的標識 unique identity

  • 狀態 state:由資料域()屬性和當前值構成

  • 和行為 behaviors: 一組function來定義的

怎麼去定義物件?

物件是類的例項(instance)

類包含什麼東西?

  • 變數定義的資料域(data fields)defined by variables

  • 函式定義的行為(Behaviors), defined by functions

A Class has two special types of functions(類中有兩種特殊的函式)

  • constructors(建構函式:在建立物件時被自動呼叫)

  • destructors (解構函式:在物件被銷燬時被自動呼叫)

簡寫為ctor 和 dtor

建立物件並訪問物件成員

建構函式(ctor)


class Circle {
public:
  // The radius of this circle
  double radius;
  
  // Construct a circle object
  Circle() {
    radius = 1;
  }
  
  // Construct a circle object
  Circle(double newRadius) {
    radius = newRadius;
  }
  
  // Return the area of this circle
  double getArea() {
    return radius * radius * 3.14159;
  }
};

Ctors的特點:

(1) Automatic invocation(自動呼叫)

(2) Has the same name as the defining class (與類同名)

(3) NO return value (including "void"); (無返回值)

(4) Can be overloaded (可過載)

(5) May have no arguments (可不帶引數)

A class may be declared without ctors (*類可不宣告建構函式)

(1) A no-arg constructor with an empty body is implicitly declared in the class. (編譯器會提供一個帶有空函式體的無參建構函式)

(2) This constructor, called *a default constructor* is provided automatically *only if no constructors are explicitly declared in the class*. (只有當未明確宣告建構函式時,編譯器才會提供這個建構函式,並稱之為“預設建構函式”)

預設建構函式

  • 沒有引數的建構函式

  • 只有當沒有明確宣告的建構函式時,編譯器才會提供這個建構函式

建立物件

注意,利用列表初始化帶有窄化檢查,有助於避免隱式型別轉換的問題

Circle circle1;   // 正確,但不推薦這樣寫
Circle  circle2(); // 錯誤!C++編譯器認為這是一個函式宣告
Circle  circle3{}; // 正確,推薦寫法。這裡面明確顯示用空初始化列表初始化circle3物件(呼叫Circle預設建構函式)
Circle circle2{ 5.5 }; // C++11 列表初始化
            // 帶有窄化檢查(narrowing check)

訪問物件

利用點運算子

#include<iostream>class Circle {//不說明時候,類中所有東西都是私有的
public://說明公有
    double radius;
    Circle() {//預設無參
        radius = 1.0;
    }
    Circle(double r) {
        radius = r;
    }
    double getArea() {
        return 3.14 * radius * radius;
    }
};
​
int main() {
    Circle c1;
    Circle c2{2.0};//用2.0調c2的建構函式,利用列表初始化
​
    std::cout << c1.getArea() << std::endl;
    std::cout << c2.getArea() << std::endl;
    return 0;
}

2、物件拷貝和分離宣告與實現

Class is a Type (類是一種資料型別)。

用類宣告一個實體的說法,與定義變數的說法有些不同:用原生資料型別定義變數,用類名定義物件。

int double 和class 都是一種資料型別

// 定義變數的例子:

// primitive data type 定義 variables
double d1;  //未初始化、不好的
double d2(3.3);  //c++03、淘汰
int  x1{2.0}; //error: Narrowing 窄化
int  x2={4};// c++11 拷貝列表初始化
auto i{x};//型別推斷,i的值賦給了x,如果x沒有被賦值,那麼會出現問題
decltype(d1) j;//根據d1的資料型別推導是double型別
// 定義物件的例子

// class 定義 objects
Circle c1;      //呼叫Circle的預設ctor(建構函式)
Circle c2(5.5); //呼叫Circle的有參ctor
Circle c3{5.5}; // 直接列表初始化,調有參ctor
Circle c4 = {5.5}; // 拷貝列表初始化,調ctor
auto c5 = Circle{2.}; // auto型別推斷,將一個匿名物件賦值給c5
decltype(c1) c6;      // decltype型別推斷

Names representing types must be in mixed case starting with upper case代表型別的名字必須首字母大寫並且與其他字母大小寫混合

Line,SavingsAccount

Memberwise Copy (成員拷貝)

How to copy the contents from one object to the other?(如何將一個物件的內容拷貝給另外一個物件)

(1) use the assignment operator( 使用賦值運算子) : =

(2) By default, *each data field* of one object is copied to its counterpart in the other object. ( 預設情況下,物件中的每個資料域都被拷貝到另一物件的對應部分)

Example: circle2 = circle1;

函式不用拷貝,就是機器程式碼,只會拷貝資料域

(1) 將circle1 的radius 拷貝到circle2 中

(2) 拷貝後:circle1 和 circle2 是兩個不同的物件,但是半徑的值是相同的。( 但是各自有一個radius 成員變數)

Anonymous Object (匿名物件)

Occasionally, you may create an object and use it only once. (有時需要建立一個只用一次的物件)

An object without name is called anonymous objects. (這種不命名的物件叫做匿名物件)

Example
​
int main() {
  Circle c1 = Circle{1.1};
  auto c2 = Circle{2.2}; // 用匿名物件做拷貝列表初始化
  
  Circle c3{};           // 直接列表初始化,調預設Ctor
  c3 = Circle{3.3};      // 用匿名物件賦值
  
  cout << "Area is " << Circle{4.2}.getAr ea() << endl;
  cout << "Area is " << Circle().getArea() << endl;  // 不推薦
  cout << "Area is " << Circle(5).getArea() << endl; // 不推薦
  return 0;
}

在c++中,類取代了struct,不同的在於struct的成員,預設是共有的,class的成員,預設是私有的(private)

Local class & Nested class (區域性類和巢狀類)

  • 區域性類是在一個函式中宣告的類

  • 巢狀類是在另一個類中宣告的類

將宣告與實現分離(Separating Declaration form implementation)

C++ allows you to separate class declaration from implementation. (C++中,類宣告與實現可以分離)

(1) .h: 類宣告,描述類的結構,放在標頭檔案

(2) .cpp: 類實現,描述類方法的實現,放在原始檔

FunctionType ClassName :: FunctionName (Arguments) { //… }

兩個冒號:: 二元運算子

其中,:: 這個運算子被稱為binary scope resolution operator(二元作用域解析運算子),簡稱“域分隔符”

當函式在類宣告中實現,它自動成為行內函數

class A {
public:
  A() = default; //C++11
  double f1() {  // f1給出函式體,自動稱為行內函數
    // do something
  } 
  double f2();
};
double A::f2() {  // f2不是行內函數
  //do something
}
class A {
public:
  A() = default; //強制要求編譯器生成一個預設(無引數)建構函式,c++
  double f1();
  double f2();
};
double A::f2() {
  //do something
}
inline double A::f1() { // f1是行內函數
  //do something
}
class A {
public:
  A() = default; //C++11
  double f1() {  // f1給出函式體,自動稱為行內函數
    // do something
  } 
  double f2();
};
double A::f2() {  // f2不是行內函數
  //do something
}
class A {
public:
  A() = default; //強制要求編譯器生成一個預設(無引數)建構函式,c++
  double f1();
  double f2();
};
double A::f2() {
  //do something
}
inline double A::f1() { // f1是行內函數
  //do something
}

避免標頭檔案被多次包含(Avoiding Multiple Inclusion of Header Files)

C/C++使用預處理指令(Preprocessing Directives)保證標頭檔案只被包含/編譯一次

// 例1:利用預處理指令

#ifndef MY_HEADER_FILE_H//如果沒有定義這個巨集
#define MY_HEADER_FILE_H
 //  標頭檔案內容
#endif
//例2:

#pragma once    // C++03, C90
// 例3

_Pragma("once") // C++11, C99;
//_Pragma實際是一個運算子,放在MAcro使用

3、物件指標、物件陣列、函式引數

指標訪問物件成員(Accessing Object Members via Pointers)

  • 物件指標可以指向新的物件名

  • 箭頭運算子 -> :用指標訪問物件成員

Circle circle1;
Circle* pCircle = &circle1; 
cout << "The radius is " << (*pCircle).radius    << endl;//對指標做解引用,訪問物件成員
cout << "The area is "   << (*pCircle).getArea() << endl;
(*pCircle).radius = 5.5;
cout << "The radius is " << pCircle->radius    << endl;//利用->訪問
cout << "The area is "   << pCircle->getArea() << endl;

在堆中建立物件

  • 在函式中宣告的物件都在棧上建立(stack)

  • 函式返回,則物件被銷燬(由編譯器管理銷燬)

  • 為保留物件,你可以用new運算子在堆(記憶體的一個區域)上建立它,程式設計師可以定義生存期,所以在建立的時候要寫一個指標存放物件的位置

Circle *pCircle1 = new Circle{}; //用無參建構函式建立物件
Circle *pCircle2 = new Circle{5.9}; //用有參建構函式建立物件,指標要是p+類的名字
//程式結束時,動態物件會被銷燬,或者
delete pObject;  //用delete顯式銷燬
#include<iostream>
#include"circle.h"int main() {
    //訪問物件指標
    auto* c1 = new Circle{1.0};
    Circle c3{ 2.0 };
    auto c2 = &c3;
    
    //std::cout << c1->getArea() << std::endl;
    std::cout << (*c1).getArea() << std::endl;
    std::cout << c2->getArea() << std::endl;
​
    //建立物件陣列
    auto c5 = new Circle[3]{ 1.0,2.0,3.0 };
    for (int i = 0; i < 3; i++)
    {
        std::cout << c5[i].getArea() << std::endl;
    }
    delete c1;
    delete [] c5;
​
    c1 = c5 = nullptr;
​
    return (0);
}

物件陣列

宣告方式:

1 類的名稱、物件名稱、陣列標識,全是呼叫預設初始CTOR(建構函式)

Circle ca1[10];

2 用匿名物件構成的列表初始化陣列,拷貝列表初始化

Circle ca2[3] = { 
    // 注意:不可以寫成: auto ca2[3]=     
    //因為宣告陣列時不能用auto
       Circle{3},
       Circle{ }, 
       Circle{5} };  

3 用C++11列表初始化,列表成員為隱式構造的匿名物件,是直接列表初始化

[兩種列表初始化的區別] https://blog.csdn.net/linda_ds/article/details/8280700
Circle ca3[3] { 3.1, {}, 5 };
//{}表明預設建構函式
Circle ca4[3] = { 3.1, {}, 5 }; 

4 用new在堆區生成物件陣列

auto* p1 = new Circle[3];
auto p2 = new Circle[3]{3.1,{},5};
delete [] p1;
delete [] p2;
p1 = p2 = nullptr;

物件作為函式引數(passing object to functions)

物件作為函式引數

  • value

  • reference

  • pointer

//物件作為函式引數 value
void print( Circle c ) {
  /**/
}
​
 int main() {
  Circle myCircle(5.0);
  print( myCircle );
  /**/
​
}
//物件引用作為函式引數
void print( Circle& c ) {
  /**/
}
​
 int main() {
  Circle myCircle(5.0);
  print( myCircle );
  /**/
}
   
//物件指標作為函式引數
void print( Circle* c ) {
  /**/
}
​
 int main() {
  Circle myCircle(5.0);
  print( &myCircle );
  /**/
}
//當我們的函式不改變物件的值,我們應該用const修飾
物件作為函式返回值
// class Object { ... };
Object f ( /*函式形參*/ ){
  // Do something
  return Object(args);
}
// main() {
//f的返回值賦值給物件
Object o = f ( /*實參*/ );
​
//main(){
f( /*實參*/ ).memberFunction();

返回物件指標,要在引數裡面傳入物件指標(右邊),然後返回,我麼要儘量用const修飾,除非有特別的含義。

用引用的形式作為返回值,注意右邊是可行的用法!!函式返回值是引用型別,儘可能用const。

總結:在為函式傳參時, 何時用引用,何時用指標呢?

一般來說,能用引用盡量不用指標。引用更加直觀,更少出現意外的疏忽導致的錯誤。

指標可以有二重、三重之分,比引用更加靈活。有些情況下,例如使用 new 運算子,只能用指標。

關於指標與引用的區別,可以看 CSDN 的這篇文章,講得很細緻;在該文中的第5部分,也講了函式傳參時“指標傳遞”與“引用傳遞”的差別,但這個解釋比較晦澀,需要你有組合語言或者微機原理或者計算機組成原理方面的知識方能透徹理解。在《深入探索C++物件模型》這本書中也有關與引用的解釋

4、抽象和封裝

封裝我們通常說的是資料域的封裝,資料域採用public的形式有2個問題

(1) First, data may be tampered. ( 資料會被類外的方法篡改)

(2) Second, it makes the class difficult to maintain and vulnerable to bugs. ( 使得類難於維護,易出現bug)

訪問器與更改器:

2.1. To read/write private data, we need get/set function (為讀寫私有資料,需要get/set函式)

(1) get function is referred to as a getter (獲取器,or accessor),

(2) set function is referred to as a setter (設定器,or mutator).

2.2. Signature of get function (General form) (get函式的一般原型)

returnType getPropertyName()

2.3. Signature of get function (Bool type) (布林型get函式的原型)

bool isPropertyName()

2.4. Signature of set function (set函式的原型)

void setPropertyName(dataType propertyValue)

類抽象與封裝:Class Abstraction and Encapsulation

在研究物件或系統時,為了更加專注於感興趣的細節,去除物件或系統的物理或時空細節/ 屬性的過程叫做抽象

封裝:

  • 一種限制直接訪問物件組成部分的語言機制

  • 一種實現資料和函式繫結的語言構造塊

總結:

抽象: 提煉目標系統中我們關心的核心要素的過程

封裝: 繫結資料和函式的語言構造塊,以及限制訪問目標物件的內容的手段

例子:circle(圓的抽象)

Abstraction(抽象):實際的圓有大小、顏色;數學上的圓有半徑(radius)、面積(area)等。抽象的過程是,將我們的關注的東西提取出來,比如:“給定半徑r,求面積”

Encapsulation(封裝):我們要限制對radius的訪問, 然後用“class”把資料和函式繫結在一起

類中的成員作用域和this指標

  • 資料成員可被所有的函式訪問

  • 資料域與函式可以按照任意順序宣告

如果成員函式中的區域性變數與某資料域同名

  • 區域性變數優先順序高:就近原則

  • 同名數據域在函式中被遮蔽

不要宣告同名變數

如何載函式內訪問類中被遮蔽的資料域?

利用this指標

  • 特殊的內建指標

  • 引用當前函式的呼叫物件

class Circle{
public:
    Circle();
    Circle(double radius)
    {
        //怎麼訪問被遮蔽的資料?
        //private裡面的radius被遮蔽了
        this->radius = radius(private 裡面的)
    }
private:
    double radius;
​
public:
    void serRadius(double);
    //
}

避免同名遮蔽的簡單方法:

5、類成員的就地初始化(Default Member Initializers)/(in class initializer)

什麼是就地初始化?在C++03標準中,只有靜態常量整型成員才能在類中就地初始化

class X {
​
  static const int a = 7;        // ok
// 靜態 常量 整形  
  const int b = 7;               // 錯誤: 非 static
  static int c = 7;              // 錯誤: 非 const
  static const string d = "odd"; // 錯誤: 非整型
  // ..
};
C++11標準中,非靜態成員可以在它宣告的時候初始化

class S { 
​
  int m = 7; // ok, copy-initializes m  
  int n(7);  // 錯誤:不允許用小括號初始化  
    
  std::string s{'a', 'b', 'c'}; // ok, direct list-initializes s
  std::string t{"Constructor run"}; // ok
    
  int a[] = {1,2,3}; // 錯誤:陣列型別成員不能自動推斷大小 
  int b[3] = {1,2,3}; // ok
    
  // 引用型別的成員有一些額外限制,參考標準
public:
  S() { } 
};

建構函式的初始化列表

(constructor initializer lists)

為了內嵌物件(OBJ IN OBJ)成員進行初始化

預設建構函式Default Constructor

預設建構函式是可以無參呼叫的建構函式,既可以是定義為空引數列表的建構函式,也可以是所有引數都有預設引數值的建構函式

class Circle1 {
public:
  Circle1() {      // 無引數
    radius = 1.0; /*函式體可為空*/
}
private:
  double radius;
};
​
class Circle2 {//有參建構函式
public:
  Circle2(double r = 1.0) // 所有引數都有預設值
    : radius{ r } {
  }
​
private:
  double radius;
};

預設建構函式的角色

若物件型別成員/內嵌物件成員沒有被顯式初始化

  • 該內嵌物件的無參建構函式會被自動呼叫

  • 若內嵌物件沒有無參建構函式,則編譯器報錯

你也可以在初始化列表中手工構造物件

若類的資料域是一個物件型別(且它沒有無參建構函式),則該物件初始化可以放到建構函式初始化列表中

成員的初始化次序

如何初始化物件/類成員

  • 就地初始化

  • 建構函式初始化列表

  • 在建構函式體中為成員賦值

執行次序: 就地初始化>Ctor初始化列表 >在Ctor 函式體中為成員賦值

哪個起作用(初始化/賦值優先順序): 在Ctor 函式體中為成員賦值 > Ctor 初始化列表 > 就地初始化

就地初始化被忽略:若一個成員同時有就地初始化和建構函式列表初始化,則就地初始化語句被忽略不執行

#include <iostream>
int x = 0;
struct S {
  int n = ++x;            // default initializer
  S() { }                 // 使用就地初始化(default initializer)
  S(int arg) : n(arg) { } // 使用成員初始化列表
​
};
int main() {
  std::cout << x << '\n'; // 輸出 0
  S s1;
  std::cout << x << '\n'; // 輸出 1 (default initializer ran)
  S s2(7);
  std::cout << x << '\n'; // 輸出 1 (default initializer did not run)
}

6、字串

C++ 使用 string 類處理字串

string類中的函式

(1) 構造

(2) 追加

(3) 賦值

(4) 位置與清除

(5) 長度與容量

(6) 比較

(7) 子 串

(8) 搜尋

(9) 運算子

注意事項

操作string物件中的字串內容時,有時會用到“index”。

很多string的函式接受兩個數字引數: index, n

(1) index: 從index號位置開始(2) n: 之後的n個字元

建立string物件

// 用無參建構函式建立一個空字串

string newString;
// 由一個字串常量或字串陣列建立string物件

string message{ "Aloha World!" };
char charArray[] = {'H', 'e', 'l', 'l', 'o', '\0'};
string message1{ charArray };

追加字串

一系列的過載函式可以將新內容附加到一個字串中

string s1{ "Welcome" };
s1.append( " to C++" ); // appends " to C++" to s1
cout << s1 << endl; // s1 now becomes Welcome to C++
string s2{ "Welcome" };
s2.append( " to C and C++", 3, 2 ); // appends " C" to s2
cout << s2 << endl; // s2 now becomes Welcome C
string s3{ "Welcome" };
s3.append( " to C and C++", 5); // appends " to C" to s3
cout << s3 << endl; // s3 now becomes Welcome to C
string s4{ "Welcome" };
s4.append( 4, 'G' ); // appends "GGGG" to s4
cout << s4 << endl; // s4 now becomes WelcomeGGGG

為字串賦值

string s1{ "Welcome" };
s1.assign( "Dallas" ); // assigns "Dallas" to s1
cout << s1 << endl; // s1 now becomes Dallas
string s2{ "Welcome" };
s2.assign( "Dallas, Texas", 1, 3 ); // assigns "all" to s2
cout << s2 << endl; // s2 now becomes all
string s3{ "Welcome" };
s3.assign( "Dallas, Texas", 6 ); // assigns "Dallas" to s3
cout << s3 << endl; // s3 now becomes Dallas
string s4{ "Welcome" };
s4.assign( 4, 'G' ); // assigns "GGGG" to s4
cout << s4 << endl; // s4 now becomes GGGG

..........還有許多方法,看cpprefence

c++的陣列類

c++原生陣列

int arr[ ] = { 1, 2, 3 };

arr 可能會退化為指標:void f(int a[]) { std::cout << sizeof(a)/sizeof(a[0]); }

arr 不知道自己的大小: sizeof(arr)/sizeof(arr[0])

兩個陣列之間無法直接賦值: array1 = array2;

不能自動推導型別:auto a1[] = {1,2,3};

c++ style array

是一個容器類,所以有迭代器(可以認為是一種用於訪問成員的高階指標)

可直接賦值

知道自己大小:size()

能和另一個數組交換內容:swap()

能以指定值填充自己: fill()

取某個位置的元素( 做越界檢查) :at()

建立C++ Style Array

C++陣列類是一個模板類,可以容納任何型別的資料

#include <array>

std::array< 陣列 型別, 陣列大小> 陣列名字;

std::array< 陣列 型別, 陣列大小> 陣列 名字 { 值1, 值2, …};

限制與C風格陣列相同

std::array<int , 10> x;

std::array<char , 5> c{ 'H','e','l','l','o' };

C++17引入了一種新特性,對類模板的引數進行推導 (學完模板才能看懂這句話)

示例:

std::array a1 {1, 3, 5}; // 推匯出 std::array<int, 3>

std::array a2 {'a', 'b', 'c', 'd'}; // 推匯出 std::array<char, 4>