程序设计实习(1) —— 类和对象

面向对象的程序设计

面向对象的程序设计方法:

  • 将某类客观事物共同特点 (属性) 归纳出来, 形成一个数据结构 (可以用多个变量描述事物的属性);
  • 将这类事物所能进行的行为也归纳出来, 形成一个个函数, 这些函数可以用来操作数据结构.

面向对象的特点有 抽象, 封装, 继承, 多态.

一般来说, 对象所占用的内存空间的大小, 等于所有成员变量的大小之和.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class CRectangle { 
public: 
    int w, h;
    int Area() { return w * h; }
    int Perimeter() { return 2 * (w + h); }
    void Init(int w_, int h_) {
        w = w_;
        h = h_;
    }
}; // 必须有分号

int main() {
    int w, h;
    CRectangle r; // r 是一个对象
    cin >> w >> h;
    r.Init(w, h);
    cout << r.Area() << endl << r.Perimeter();
    return 0;
}

和结构变量一样, 对象之间可以用 = 进行赋值, 但是不能用 ==, !=, >, <, >=, <= 进行比较, 除非这些运算符经过了 重载.

  • 对象名.成员名
1
2
3
CRectangle r1, r2;
r1.w = 5;
r2.Init(5, 4);
  • 指针->成员名
1
2
3
4
5
CRectangle r1, r2;
CRectangle *p1 = &r1;
CRectangle *p2 = &r2;
p1->w = 5;
p2->Init(5, 4);
  • 引用名.成员名
1
2
3
CRectangle r1;
CRectangle &rr = r1;
rr.w = 5;

引用

引用名是对象名的别名, 指向同一个对象. 语法: 类名 &引用名 = 对象名; 引用的好处是可以减少指针的使用, 使得代码更加简洁.

1
2
3
4
5
6
7
8
9
// 要用指针, 否则参数传递会产生拷贝
void swap(int *a, int *b) {
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}
int n1, n2;
swap(&n1, &n2);

可以改写为:

1
2
3
4
5
6
7
8
void swap(int &a, int &b) {
    int tmp;
    tmp = a;
    a = b;
    b = tmp;
}
int n1, n2;
swap(n1, n2);

引用还可以作为函数的返回值.

1
2
3
4
5
6
int &ref(int &a) {
    return a;
}

int n = 5;
ref(n) = 6; // n = 6

常量, 常引用, 常量指针

定义引用时, 前面加 const 关键字, 表示 常引用. 不能通过常引用修改对象的值, 即只读引用.

1
2
3
4
int n;
const int &r = n;
r = 5; // 错误
n = 5; // 正确
  • const T&T& 是不同的类型. T& 类型的引用或 T 类型的变量可以用来初始化 const T& 类型的引用.
  • const T 类型的常变量和 const T& 类型的引用则不能用来初始化 T& 类型的引用, 除非进行强制类型转换.

不可通过常量指针修改其指向的内容, 但可以修改指针的指向 (引用不可以).

1
2
3
4
5
int n, m;
const int *p = &n;
*p = 5; // 错误
n = 5; // 正确
p = &m; // 正确

函数参数为常量指针时, 可避免函数内部不小心改变参数指针所指地方的内.

1
2
3
4
void myPrintf(const int *p) {
    *p = 5; // 错误
    p = &m; // 正确
}

类成员的访问控制

  • public: 公有成员, 可以在类的外部访问.
  • private: 私有成员, 只能在类的内部访问, 缺省默认为 private.
  • protected: 保护成员, 只能在类的内部和派生类中访问.
1
2
3
4
5
6
7
8
9
class className {
// 这三个关键字可以出现多次, 没有顺序要求
private:
// 私有属性和函数
public:
// 公有属性和函数
protected:
// 保护属性和函数
};

类内部可以访问当前对象和同类其他对象的私有成员.

“隐藏” 的目的是强制对成员变量的访问一定要通过成员函数进行, 那么以后成员变量的类 型等属性修改后, 只需要更改成员函数即可.

structclass 的唯一区别是默认的访问控制权限不同, struct 默认为 public, class 默认为 private.

函数重载和缺省参数

函数名相同, 参数个数或类型不同, 注意没有返回值类型不同.

1
2
3
double _max(double f1, double f2);
int _max(int n1, int n2);
int _max(int n1, int n2, int n3);

C++ 中, 定义函数的时候可以让 最右边 的连续若干个参数有缺省值, 那么调用函数的时候, 若相应位置不写参数, 参数就是缺省值.

1
2
3
4
void func(int a, int b = 0, int c = 0);
func(1); // a = 1, b = 0, c = 0
func(1, 2); // a = 1, b = 2, c = 0
func(1, , 8); // 错误

成员函数也可以重载或有缺省参数.

构造函数

构造函数是一种特殊的成员函数: 名字与类名相同, 没有返回值 (void 也不行). 作用是初始化对象的数据成员.

如果定义类时没有定义构造函数, 编译器会生成一个默认的无参构造函数. 如果定义了, 默认构造函数就不会生成. 对象生成时, 构造函数自动调用. 生成之后不能再执行构造函数. 一个类可以有多个构造函数, 可以重载.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Complex {
private:
    double real;
    double imag;
public:
    void Set(double r, double i);
}; //编译器自动生成默认构造函数

// 两种写法均可.
Complex c1;
Complex *pc = new Complex;

手动加入构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r, double i = 0) { // 带缺省参数
        real = r;
        imag = i;
    }
};

Complex *pc1 = new Complex; // error, 没有参数
Complex c2(2); // OK
Complex *pc2 = new Complex(3, 4);

构造当然也可以 private, 这样就不能用来生成对象, 但是可以用来实现单例模式.

构造函数还可以用在数组.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class CSample { 
    int x;
public:
    CSample() {}
    CSample(int n) { x = n; }
    CSample(int m, int n) { x = m + n; }
};

int main() {
    CSample array1[2]; // 两次无参
    CSample array2[2] = {4, CSample(5, 3)}; // 两次有参
    CSample array3[2] = {3}; // 一次有参一次无参
    CSample *array4 = new CSample[2];
    delete[] array4;
    return 0;
}

复制构造函数只有一个参数, 且参数是本类的引用 (或常量引用). 如果没有定义, 编译器会生成一个默认的复制构造函数. 如果定义了, 默认复制构造函数就不会生成.

1
2
3
4
5
6
7
8
class Complex {
private:
    double real;
    double imag;
};

Complex c1;
Complex c2(c1); // 默认复制构造函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Complex {
public:
    double real;
    double imag;
    Complex() {} // 必须要写, 否则编译器不会生成默认构造函数
    Complex(const Complex &c) {
        real = c.real;
        imag = c.imag;
        cout << "Copy Constructor called";
    }
};
Complex c1;
Complex c2(c1); // 自定义复制构造函数

复制构造函数的调用时机:

  • 用一个对象去初始化另一个对象.

    1
    2
    
    Complex c1;
    Complex c2(c1);
    
  • 一个对象作为函数参数传递给一个非引用类型的参数.

    1
    2
    3
    
    void func(Complex c);
    Complex c1;
    func(c1);
    
  • 一个对象作为函数返回值返回.

    1
    2
    
    Complex func();
    Complex c1 = func();
    

注意: 对象之间的赋值操作, 不会调用复制构造函数.

1
2
Complex c1, c2;
c1 = c2; // 不会调用复制构造函数

考虑到对象作为函数参数会掉用复制构造函数, 为了避免不必要的开销, 可以使用引用传递.

手动写复制构造函数的目的一般是为了实现深拷贝.

转换构造函数的目的是实现类型的自动转换. 不以说明符 explicit 声明 {且可以用单个参数调用 (C++11 前)} 的构造函数被称为转换构造函数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Complex {
public:
    double real, imag;
    Complex(int i) { // 类型转换构造函数
        real = i;
        imag = 0;
    }
};
int main() {
    Complex c1 = 9; // 隐式调用, 转换成一个临时 Complex 对象
    return 0;
}

如果加了 explicit 关键字, 则只能显式调用.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Complex {
public:
    double real, imag;
    explicit Complex(int i) { // 类型转换构造函数
        real = i;
        imag = 0;
    }
};
int main() {
    Complex c1 = 9; // 错误
    Complex c2(9); // 正确
    return 0;
}

析构函数

析构函数是类的一个特殊成员函数, 名字由波浪号 ~ 加类名构成, 没有参数, 也没有返回值, 对象消亡时即自动被调用, 作用是释放对象所占用的资源.

如果定义类时没写析构函数, 则编译器生成缺省析构函数.缺省析构函数什么也不做. 如果定义了析构函数, 缺省析构函数就不会生成. 一个类只有一个析构函数, 不能重载.

在数组生命周期结束时, 编译器会自动调用数组中每个元素的析构函数. delete 一个对象时, 会调用对象的析构函数. (注意, new 数组要用 delete[])

1
2
3
4
5
Ctest *pTest;
pTest = new Ctest; // 构造函数调用
delete pTest; // 析构函数调用
pTest = new Ctest[3]; // 构造函数调用 3 次
delete[] pTest; // 析构函数调用 3 次

this 指针

this 是一个指向对象本身的指针. 把 car.foo() 翻译成 C 就是 foo(&car)

1
2
3
4
5
6
7
8
9
class A {
    int i;
public:
    void hello() { cout << "hello" << endl; }
};
int main() {
    A *p = NULL;
    p->hello();
}

能运行且输出, 但是是未定义行为. 而且一旦 hello() 中用到了 this, 就会出错.

非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Complex {
public:
    double real, imag;
    void print() {
        cout << real << "," << imag;
    }
    Complex(double r, double i) : real(r), imag(i) {}
    Complex addOne() {
        this->real++; // 等价于 real++;
        this->print(); // 等价于 print()
        return *this; // 返回对象本身
    }
};

静态成员

静态成员即加了 static 关键字的成员.

静态成员变量是类的所有对象共享的. sizeof 不包括静态成员变量. 本质是全局变量. 静态成员函数是类的所有对象共享的函数, 静态成员函数只能访问静态成员变量和静态成员函数, 不能访问普通成员变量和普通成员函数, 也不可以用 this 指针. 本质是全局函数.

访问静态成员可以不通过对象访问. 类名::静态成员名.

1
2
int Rectangle::edges = 4;
Rectangle::printTotal();

即使类的对象不存在, 静态成员变量也存在.

成员对象和封闭类

有成员对象的类叫封闭类.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class CTyre {
private:
    int radius;
    int width;
public:
    CTyre(int r, int w) : radius(r), width(w) {} 
}; 
class CEngine {};
class CCar {
private:
    int price; // 价格
    CTyre tyre;
    CEngine engine;
public:
    CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int w) : price(p), tyre(tr, w) {}

这个例子 CCar 必须有构造函数, 因为 CTyre 和没有默认构造函数.

  • 封闭类对象生成时, 先执行所有对象成员的构造函数, 然后才执行封闭类的构造函数.
  • 对象成员的构造函数调用次序和对象成员在类中的说明次序一致, 与它们在成员初始化列表中出现的次序无关.
  • 当封闭类的对象消亡时, 先执行封闭类的析构函数, 然后再执行成员对象的析构函数. 次序和构造函数的调用次序相反.
  • 封闭类的对象, 如果是用默认复制构造函数初始化的, 那么它里面包含的成员对象也会用复制构造函数初始化.

友元

友元分为友元函数和友元类两种.

友元函数: 一个类的友元函数可以访问该类的私有成员.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CCar; // 提前声明 CCar 类, 以便后面的 CDriver 类使用
class CDriver {
public:
    void modifyCar(CCar *pCar);
};
class CCar {
private:
    int price;
    friend int mostExpensiveCar(CCar cars[], int total); // 声明友元
    friend void CDriver::modifyCar(CCar *pCar); // 声明友元
    // 可以将一个类的成员函数 (包括构造/析构函数) 说明为另一个类的友元。
};
void CDriver::modifyCar(CCar *pCar) {
    pCar->price += 1000;
}
int mostExpensiveCar(CCar cars[], int total) {
    int tmpMax = -1;
    for (int i = 0; i < total; ++i)
        if (cars[i].price > tmpMax)
            tmpMax = cars[i].price;
    return tmpMax;
}

如果 A 是 B 的友元类, 那么 A 的成员函数可以访问 B 的私有成员.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class CCar {
private:
    int price;
    friend class CDriver; // 声明 CDriver 为友元类
};
class CDriver {
public:
    CCar myCar;
    void modifyCar() {
        myCar.price += 1000; // 因 CDriver 是 CCar 的友元类, 故此处可以访问其私有成员
    }
};

友元类之间的关系不能传递, 不能继承.

常量对象, 常量成员函数

如果不希望某个对象的值被改变, 则定义该对象的时候可以在前面加 const 关键字变为常量对象.

在类的成员函数说明后面可以加 const 关键字, 则该成员函数成为常量成员函数. 常量成员函数内部不能改变属性的值, 也不能调用非常量成员函数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Sample {
private:
    int value;
public:
    void func() {};
    Sample() {}
    void SetValue() const {
        value = 0; // wrong
        func(); // wrong
    }
};
const Sample Obj;
Obj.SetValue(); // 常量对象上可以使用常量成员函数

对于

1
2
3
4
5
6
int getValue() const {
    return n;
}
int getValue() {
    return 2 * n;
}

两个函数, 名字和参数表都一样, 但是一个是 const, 一个不是, 算重载.

加上 mutable 关键字的成员变量, 即使在常量成员函数中也可以被修改.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class CTest {
public:
    bool getData() const {
        m_n1++;
        return m_b2;
    }
private:
    mutable int m_n1;
    bool m_b2;
};
本文遵循 CC BY-NC-SA 4.0 协议
使用 Hugo 构建