[c++] Class
封装 Encapsulation、继承 Inheritance、多态 Polymorphism
菜鸡三特征
菜鸟教程阅读笔记,有没有什么重难点 Since C++ 类 & 对象。
一、封装性
基本概念
到目前为止,我们已经对 C++ 的类和对象有了基本的了解。下面的列表中还列出了其他一些 C++ 类和对象相关的概念,可以点击相应的链接进行学习。
概念 | 描述 |
---|---|
类成员函数 | 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。 |
类访问修饰符 | 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。 |
构造函数 & 析构函数 | 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。 |
C++ 拷贝构造函数 | 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。 |
C++ 友元函数 | 友元函数 可以访问类的 private 和 protected 成员。 |
C++ 内联函数 | 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。 |
C++ 中的 this 指针 | 每个对象都有一个特殊的指针 this,它指向对象本身。 |
C++ 中指向类的指针 | 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。 |
C++ 类的静态成员 | 类的数据成员和函数成员都可以被声明为静态的。 |
class 封装破坏
破坏原有规则的 "类型",实现在外部,但当作是类的自己人.
* 友元可以是一个函数,该函数被称为友元函数;
* 友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
* 友元关系在类之间不能传递,即类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元。“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上 不成立。
- 友元函数
Ref: C++ 友元函数
不是类的成员函数,在此,只是类中的一个声明。所以自然也就没有 "this pointer"。
因为友元函数 没有this指针,则参数要有三种情况:
-
-
- 要访问非static成员时,需要对象做参数;
- 要访问static成员或全局变量时,则不需要对象做参数;
- 如果做参数的对象是全局对象,则不需要对象做参数;
- 可以直接调用友元函数,不需要通过对象或指针。
-
class Box
{
double width;
public:
double length;
friend void printWidth( Box box ); // 对象直接作为参数
void setWidth( double wid );
};
- 友元类
在这个例子中,友元类可以访问该类的private内容.
class CCar
{
private:
int price;
friend class CDriver; //声明 CDriver 为友元类
};
class CDriver
{
public:
CCar myCar;
void ModifyCar() // 改装汽车
{
myCar.price += 1000; // 因CDriver是CCar的友元类,故此处可以访问其私有成员
}
};
int main()
{
return 0;
}
const 常量破坏
mutable 类型,实现在内部,但不是自己人.
(1) 定义
(2) 清空队列/bash table等,如果在多线程时可能会涉及到”mutex“,这个变量就have to be "mutable"!
(3) 一码归一码。在类的内部 不一定就是”自己人“。
const意思是“这个函数不修改对象内部状态”。 为了保证这一点,编译器也会主动替你检查,确保你没有修改对象成员变量——否则内部状态就变了。 mutable意思是“这个成员变量不算对象内部状态”。 比如,你搞了个变量,用来统计某个对象的访问次数(比如供debug用)。它变成什么显然并不影响对象功用,
-- 但编译器并不知道:
它仍然会阻止一个声明为const的函数修改这个变量。 -- 把这个计数变量声明为mutable,编译器就明白了:
这个变量不算对象内部状态,修改它并不影响const语义,所以就不需要禁止const函数修改它了
二、继承性
继承类型
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
-
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
多继承
Goto: C++多继承中的二义性问题
(a) 两个父类中都有同名的成员函数
a) son.Parent_f::show(); 如此指明是哪个父类的函数。(子对象.父类名::函数名)
b) 在类中定义同名成员,直接覆盖掉父类中的相关成员,使用自己的就好。
(b) “菱形”问题——路径二义性
Ref: C++:钻石继承与虚继承【比较详细】
父类对祖父类的继承方式改为 "虚继承",那么子类访问自己从祖父类那里继承过来的成员就不会有二义性问题了,也就是将子类对象里的祖父类对象统一为一个,继承的只有一个祖父类对象。
class A{ public: int t; A(int a) { t = a; } void fun(); }; class B:virtual public A { public: B(int a, int b) :A(a+10) { t1 = b; } int t1; }; class C :virtual public A { public: C(int a,int c):A(a+20) { t2 = c; } int t2; }; class D :public B,public C { public: D(int a,int b,int c,int d) :B(a,b),C(a,c),A(a){} }; int main() { D temp(10,2,3,4); cout << temp.t << endl; B b(100, 200); cout << b.t << endl; cout << "hello world." << endl; return 0; }
三、多态性
virutal function
- 后期绑定 - "虚函数"
编译时绑定,所以子类的函数指针过早的链接了父类。(静态多态,早绑定)
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器 "不要" 静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为 "动态链接",或 "后期绑定"。
class A { public: virtual void foo() { cout<<"A::foo() is called"<<endl; } };
class B: public A { public: void foo() { cout<<"B::foo() is called"<<endl; } };
int main(void) { A *a = new B(); a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的! return 0; }
- 抽象类 - "纯虚函数"
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的
class Shape {
protected: int width, height;
public: Shape( int a=0, int b=0) { width = a; height = b; }
// pure virtual function virtual int area() = 0; };
设计理念
- 数据抽象
"只向外界提供关键信息,并隐藏其后台的实现细节" 就是 纯虚函数 实现接口 的一种表现形式。
依赖于 "接口和实现分离" 的编程 (设计) 技术。
- 数据封装
C++中, 虚函数 可以为private, 并且可以被子类覆盖(因为虚函数表的传递),但子类不能调用父类的private虚函数。虚函数的重载性和它声明的权限无关。
一个成员函数被定义为private属性,标志着其只能被当前类的其他成员函数(或友元函数)所访问。
virtual 修饰符则强调 父类的成员函数可以在子类中被重写,因为重写之时并没有与父类发生任何的调用关系,故而重写是被允许的。
编译器不检查虚函数的各类属性。被virtual修饰的成员函数,不论他们是private、protect或是public的,都会被统一的放置到虚函数表中。对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数表(相同偏移地址指,各虚函数相对于VPTR指针的偏移),则子类就会被允许对这些虚函数进行重载。且重载时可以给重载函数定义新的属性,例如public,其只标志着该重载函数在该子类中的访问属性为public,和父类的private属性没有任何关系!
纯虚函数 可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利。
这里的例子用到了“友元”,也必须有。
简单說,父类代理的特性只让个别的friend使用,那么首先把virtual设置为private,然后再设置friend即可.
From: 虚函数用作private会怎样?
#include <iostream> using std::cout; using std::endl; class Bclass { private: virtual void fun() { cout << "Bclass fun is here!" << endl ; } friend int main(); # 声明一下 }; class Dclass : public Bclass { public: void fun() { cout << "Dclass fun is here!" << endl; } }; int main() { Bclass *pObject = new Dclass(); pObject->fun(); // "Dclass fun is here!" return 0; }
实现一个类
一、定义这个类
大纲:"也是醉了,一个 .h文件 就有这么多细节问题 ".
头文件中对类的定义所涉及到的一些问题:
* 初始化列表,使用{} 也可以。
* 类中的引用和const变量,必须立即在初始化列表中提前初始化。
* 常成员函数,const 放在函数后, 常成员函数即不能改变成员变量值的函数。例如 getxxx() const;
* mutable类型,当需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。例如 class中的计数器,类中函数都用,包括const函数。【破坏规则的设计】
* 友元类,其所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。【破坏规则的设计】
看来 const 会带来一些注意点.
-
常数据成员
原因:
(1) 如果我们不修改Base,不去调用默认构造函数,而是显式的调用Base自带参构造函数呢?答案就是初始化列表。
(2) const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
区别:
首先把数据成员按类型分类并分情况说明:
(1). 内置数据类型,复合类型(指针,引用)- 在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。
(2). 用户定义类型("类"类型)- 结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
初始化列表
- 成员初始化顺序
C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
// 典型的容易犯错的例子
class CMyClass { CMyClass(int x, int y); intint m_y; }; CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y) { }
- 提高效率
class A { public: int a; A() { cout<<"construct"<<endl; } A(A& b) { cout<<"copy construct"<<endl; } A& operator = (const A& x){ cout<<"operator"<<endl; return *this; } }; class B { public: A a; // 方案一 // B(A &m) { // a = m; // 这里触发了 operator 类的赋值过程 // } // 方案二 --> 原理是:a(m)在构造阶段,而非计算阶段执行 B(A &m):a(m) { } }; int main() { A a; B B(a); }
两方案的区别是什么? 方案二 在构造阶段解决战斗;方案一 还需要一次计算。
方案一 construct construct operator
方案二 construct copy construct
- 类型转换
初始化最正确的方式 narrowing and casting
a) 避免 -Wall -Werror always warn.
b) -Werror=narrowing
int i ( static_cast<int> (aDouble) )
-
常成员函数
From: C++常对象与对象成员
From: C++常对象,常变量,常成员函数详解(含添加内容)【有常见错误例子,不错】
常成员函数特点:
* 不能 更新任何数据成员,
* 也不能 调用该类中没有用const修饰的成员函数,
* 只能 调用常成员函数和常数据成员。
其他特点:
* const是函数类型的一部分,在实现部分也要带该关键字。
* const关键字的重载。
* 常成员函数不能更新任何数据成员。
* 常成员函数可以被其他成员函数调用。
* 但是不能调用其他非常成员函数。
* 可以调用其他常成员函数。
-
常对象
她的特点:
* 常对象是指对象的数据成员的值在对象被调用时不能被改变。
* 常对象必须进行初始化,且不能被更新。
* 不能通过常对象调用普通成员函数,但是可以通过普通对象调用常成员函数。
* 常对象只能调用常成员函数。
什么时候用?
既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const。
-
常引用
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
也就是说,一个实际的一小块内存,通过原变量可以改变;通过“影子”引用则不可以。
头文件中,实现 Class Sales_data 的声明。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <iostream>
using namespace std;
class Sales_data {
public:
//construcitors
Sales_data(const string &s):bookName{s}
{
}
Sales_data(const string &s, unsigned n, double p):bookName{s}, units_sold{n}, revenue{p*n}
{
}
Sales_data(void):Sales_data{"", 0, 0} // 构造函数 再调用 上面这个构造函数
{
}
Sales_data(istream &);
// 常成员函数
string getBookName() const;
Sales_data& combine(const Sales_data &);
double avg_price() const;
//----------------------------------------------------------------------
// 通过"人工输入"来填充这个对象的内容
friend istream& read(istream &is, Sales_data &item);
friend ostream& print(ostream &os, const Sales_data &item);
private:
string bookName;
unsigned units_sold {0};
double revenue {0.0};
mutable unsigned no_of_times_called {0};
};
#endif // SALES_DATA_H
二、实现这个类
成员函数等的实现。
#include "sales_data.h" using namespace std;
// Declare istream& read(istream &is, Sales_data &item); ostream& print(ostream &os, const Sales_data &item); // member functions Sales_data::Sales_data(istream &is) { read(is, *this); } // const member functions string Sales_data::getBookName() const { ++no_of_times_called; return bookName; } double Sales_data::avg_price() const { if (units_sold) { return revenue/units_sold; } else { return 0; } } Sales_data& Sales_data::combine(const Sales_data &rhs) { cout << "combine() in." << endl; units_sold += rhs.units_sold; revenue += rhs.revenue; cout << "combine() out." << endl; return *this; } /* * Non-member class related function. * These need to be decleared as a 'friend' to work. */ istream& read(istream &is, Sales_data &item) { cout << "read() in." << endl; double price {0}; is >> item.bookName >> item.units_sold >> price; item.revenue = price * item.units_sold; cout << "read() out." << endl;
// 技巧:当输入不符合规范,返回false的is. return is; } ostream& print(ostream &os, const Sales_data &item) { cout << "print() in." <<<< item.bookName << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); cout << "print() out." << endl; return os; }
this 指针
Ref: http://c.biancheng.net/view/170.html
C++ 是在C语言的基础上发展而来的,第一个 C++ 的编译器实际上是将 C++ 程序翻译成C语言程序,然后再用C语言编译器进行编译。
在non-const成员函数中
在const成员函数中,自动被const。
类的使用 main.cpp.
#include <iostream>
#include "sales_data.h"
using namespace std;
int main(void)
{
cout << "hello world." << endl;
Sales_data curBook{"Harry Potter"};
if (read(std::cin, curBook))
{
Sales_data newBook{""};
while (read(std::cin, newBook))
{
cout << "While()..." << endl;
if (curBook.getBookName() == newBook.getBookName())
{
curBook.combine(newBook);
}
else
{
print(cout, curBook) << endl;
curBook = newBook;
}
}
print(cout, curBook) << endl;
}
else
{
cerr << "No data?!" << endl;
}
return 0;
}
End.