CPP 14. C++中的代码重用 2020-11-26 浏览量 684 暂无评论 # 第14章 C++中的代码重用 🐸小知识: 1. 获得接口是is-a关系的组成部分,不继承接口是has-a关系的组成部分; 2. 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序; 3. 实现has-a关系的途径:包含、私有继承; 4. 通常,使用包含来建立has-a关系,如果需要访问原有类的保护成员,或需重新定义虚函数,则使用私有继承; ## 14.1 包含对象成员的类 **valarray模板类** ```cpp double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3}; valarray v1; valarray v2(8); valarray v3(10, 8); // 8个整型元素,每个都为10 valarray v4(gpa, 4); // 4个双浮点型,gpa前四个 operator[]() size() sum() max() min() ``` ## 14.2 私有继承 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,派生类不继承基类的接口。 使用强制类型转换来访问基类对象 ```cpp class Student : private std::string, private std::valarray { public: ... }; ``` ### 14.2.1 保护继承:保护继承是私有继承的变体 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员,这样就可以在第三代派生类中使用基类的公有方法。 ![](http://lcipm.com/Jineng/picture-bed/uploads/ab2330cd4dc16379476536567505fa82/20201124194025.png) ### 14.2.2 使用using重新定义访问权限 目的:让基类的方法在派生类外面可用 1. 定义一个使用该基类方法的派生类方法; ```cpp double Student::sum() const { return std::valarray::sum(); } ``` 2. 使用一个using声明来指出派生类可以使用特定的基类成员 ```cpp class Student : private std::string, private std::valarray { public: using std::valarray::min; using std::valarray::max; }; ``` ## 14.3 多重继承(MI) 需要处理的问题: 1. 从两个或更多相关基类那里继承同一个类的多个实例; ```cpp SingingWaiter ed; Worker * pw = &ed; 通常,这种赋值将把基类指针设置为派生类对象中的基类对象的地址,但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象;(这样将会很复杂) Worker * pw1 = (Waiter *) &ed; Worker * pw2 = (Singer *) &ed; ``` 幸好,C++引入多重继承的同时,引入了一种新技术--虚基类,它使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。 ```cpp class Singer : virtual public Worker {...}; class Waiter : public virtual Worker {...}; ``` 这样做会出现一个问题:派生类构造函数在向基类传递参数时,由于有2条不同的途径,会产生冲突,所以C++在基类是虚的时,禁止信息通过中间类自动传递给基类。 ```cpp SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {} 上构造函数将初始化Waiter的成员panache和Singer的成员voice,但wk参数中的信息不会传递给子对象Waiter,这时,编译器将使用Worker的默认构造函数。 SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, o), Singer(wk, v) ``` 2. 从两个不同的基类继承同名方法 a. 使用作用域解析运算符 ```cpp SingingWaiter newhire("jhan", 2005, 6, soprano); newhire.Singer::Show(); ``` b. 重新定义 ```cpp void SingingWaiter::Show() { Singer::Show(); } ``` ## 14.4 类模板 生成通用的类声明。模板的具体实现被称为实例化或具体化。 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。 ```cpp // stacktp.h -- a stack template #ifndef STACKTP_H_ #define STACKTP_H_ template class Stack { private: enum {MAX = 10}; Type items[MAX]; int top; public: Stack(); bool isempty(); bool isfull(); bool push(const Type & item); bool pop(Type & item); }; template Stack::Stack() { top = 0; } template bool Stack::isempty() { return top == 0; } template bool Stack::isfull() { return top == MAX; } template bool Stack::push(const Type & item) { if (top < MAX) { items[top++] = item; return true; } else return false; } template bool Stack::pop(Type & item) { if (top > 0) { item = items[--top]; return true; } else return false; } // stackem.cpp -- testing the template stack class #include #include #include #include "stacktp.h" using std::cin; using std::cout; int main() { Stack st; char ch; std::string po; ... } ``` 其他类型的模板头: ```cpp 1. template 第二个参数称为非类型或表达式参数,表达式参数可以使整型、枚举、引用或指针。 缺点:每种数组大小都生成自己的模板,即不同的n将生成不同的独立的类声明。 2. template 3. template class Topo { ... }; ``` 模板的具体化 1. 隐式实例化 `ArrayTP stuff;` 2. 显式实例化 `template class ArrayTP;` 3. 显式具体化 用于为特殊类型实例化时,对模板进行修改,使其行为不同。 `template <> class Classname { ... };` **成员模板** ```cpp // tempmemb.cpp -- template members #include using std::cout; using std::endl; template class beta { private: template class hold { private: V val; public: hold(V v = 0) : val(v) {} void show() const {cout << val << endl;} V Value() const {return val;} }; hold q; hold n; public: beta(T t, int i) : q(t), n(i) {} // 模板函数 template U blab(U u, T t) {return (n.Value() + q.Value()) * u / t;} void Show() const {q.show(); n.show();} }; q成员是基于T类型的hold对象,blab()方法的U类型由该方法被调用时的参数值显示确定,T类型是由对象的实例化类型确定。 ``` 模板用作参数 ```cpp template class Thing> class Crab { private: Thing s1; Thing s2; public: Crab() {}; bool push(int a, double x) {return s1.push(a) && s2.push(x);} bool pop(int & a, double & x) {return s1.pop(a) && s2.pop(x);} }; Crab nebula; // Thing将被实例化为Stack ``` 头疼,模板类和友元,每种都适合于什么样的场景呢? 非模板友元函数、约束模板友元函数、非约束模板友元函数 - 阅读全文 -
CPP 13. 类继承 2020-11-19 浏览量 734 暂无评论 # 第13章 类继承 🐸小知识: 1. 以前一直以为继承跟直接复制再添加没啥区别,而且就算使用继承,也得先了解基类的特性,熟悉基类,现在有一点让我对继承有了新的看法:它甚至不需要访问源代码就可以派生出类,可以在不公开实现的情况下将自己的类分发给其他人,不过对个人来讲,这没啥用; 2. 派生类需要自己的构造函数; 3. 基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显式类型转换的情况下引用派生类对象,但是只能调用基类方法; 4. C++继承的3种方式:公有继承、保护继承、私有继承; 5. 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法; 6. 通常为基类声明一个虚析构函数,这样做是**为了确保释放派生对象时,按正确的顺序调用析构函数(怎么确保呢?)**; 7. 关键字virtual只用于类声明的方法原型中,而不用在定义中; ## 13.1 简单基类 ```cpp // tabtenn0.h #ifndef TABTENN0_H_ #define TABTENN0_H_ #include using std::string; class TableTennisPlayer { private: string firstname; string lastname; bool hasTable; public: TableTennisPlayer (const string & fn = "none", const string & ln = "none", bool ht = false); void Name() const; bool HasTable() const {return hasTable;}; void ResetTable(bool v) {hasTable = v;}; }; // tabtenn0.cpp #include "tabtenn0.h" #include TableTennisPlayer::TableTennisPlayer (const string & fn, const string & ln, bool ht) : firstname(fn), lastname(ln), hasTable(ht) {} void TableTennisPlayer::Name() const { std::cout << lastname << ", " << firstname; } ``` **派生一个类** ```cpp // 公有派生:基类的公有成员将成为派生类的公有成员, // 基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问 class RatedPlayer : public TableTennisPlayer { private: unsigned int rating; public: RatedPlayer (unsigned int r = 0, const string & fn = "none", const string & ln = "none", bool ht = false); RatedPlayer (unsigned int r, const TableTennisPlayer & tp); unsigned int Rating() const {return rating;} void ResetRating (unsigned int r) {rating = r;} }; // 创建派生类对象时,程序首先创建基类对象, // 这意味着基类对象应当在程序进入派生类构造函数之前被创建 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht) { rating = r; } RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp) { rating = r; } // 如果不调用基类构造函数,程序将使用默认的基类构造函数 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) { rating = r; } 等价于 RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer() { rating = r; } ``` **基类指针与引用** ```cpp RatedPlayer rplayer(1140, "Jineng", "Han", true); TableTennisPlayer & rt = rplayer; TableTennisPlayer * pt = &rplayer; rt.Name(); pt->Name(); rt.Rating(); // invalid ``` ## 13.2 继承:Is-a关系 C++继承的3种方式:公有继承、保护继承、私有继承。 公有继承建立一种is-a-kind-of关系(is-a,香蕉是一种水果),不建立has-a关系(午餐有水果),不能建立is-like-a关系(我像小太阳),不建立is-implemented-as-a关系(用数组实现栈),不建立uses-a关系(计算机使用打印机)。 ## 13.3 多态公有继承 它是为了处理这样的情况的:希望同一个方法在派生类和基类中的行为不同。 可以使用两种机制实现多态公有继承: 1. 在派生类中重新定义基类的方法; 2. 使用虚方法(经常在基类中将派生类会重新定义的方法声明为虚方法); ```cpp // brass.h #ifndef BRASS_H_ #define BRASS_H_ #include // 基类中也要加关键字virtual?修改不方便啊,谁知道写基类的时候哪个方法要被重用呢 class Brass { private: std::string fullName; long accNum; double balance; public: Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0); void Deposit(double amt); virtual void Withgraw(double amt); double Balance() const; virtual void ViewAcct() const; virtual ~Brass() {} }; class BrassPlus : public Brass { private: double maxLoan; double rate; double owesBank; public: BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.11125); BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125); virtual void ViewAcct() const; virtual void Withgraw(double amt); void ResetMax(double m) {maxLoan = m;} void ResetRate(double r) {rate = r;} void ResetOwes() {owesBank = 0;} }; #endif ``` 如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法,例如: ```cpp 如果ViewAcct()不是虚的: Brass dom("jhan", 123421, 12435.65); BrassPlus dot("han"m 12434, 1243.54); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use Brass::ViewAcct() 如果ViewAcct()是虚的: b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use BrassPlus::ViewAcct() ``` ## 13.4 静态联编和动态联编 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象。 **虚函数的工作原理:** 实现虚函数功能的是一个保存了指向函数地址数组的指针的隐藏成员(虚函数表),若派生类重新定义了基类的虚函数,则派生类的虚函数表存储的地址替换为新的虚函数地址,否则地址仍为基类虚函数的地址,调用函数时,到对应的虚函数表中查找地址。 **注意点:** 1. 构造函数不能是虚函数; 2. 析构函数应当是虚函数,除非类不用做基类,要不然指向基类的指针就不能调用派生类的析构函数来释放内存了; 3. 友元不能是虚函数,因为友元不是类成员; 4. 如果派生类没有重新定义函数,将使用该函数的基类版本,如果派生类位于派生链中,则将使用最新的虚函数版本(递进原则); 5. 派生类的虚函数将隐藏基类的; ## 13.5 访问控制:protected protected与private的共同点:在类外只能用公有类成员来访问protected部分中的类成员; protected与private的区别:派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。 **小提示:** 尽量对类数据成员采用私有访问控制。 ## 13.6 抽象基类 用来处理两个类有很多共同点,但实际继承时又有许多麻烦的情况,这时,可以从这两个类中抽象出来它们的共性,将这些特性放到一个ABC中。通过纯虚函数来实现ABC。 🐸小知识: 1. 当类声明中包含纯虚函数时,不能创建该类的对象,包含纯虚函数的类只用作基类; 2. 纯虚函数的结尾处为=0,`virtual double Area() const = 0;`; 3. 如果所有共性的基类方法都一样,可以通过将原型声明为虚的来使基类成为抽象的`void Move(int nx, ny) = 0;`; ## 13.7 继承和动态内存分配 如果基类使用动态内存分配,并重新定义赋值和复制构造函数,派生类的实现会受到怎样的影响? 1. 派生类不使用new 不需要为派生类定义显式析构函数、复制构造函数和赋值运算符,默认的就能满足了; 2. 派生类使用new 必须为派生类定义显式析构函数、复制构造函数和赋值运算符。 1. 析构函数:依赖于基类的析构函数释放基类中的指针数据成员,并释放派生类指针数据成员管理的内存。 ```cpp baseDMA::~baseDMA() { delete [] label; } hasDMA::~hasDMA() { delete [] style; } ``` 2. 复制构造函数 复制构造函数baseDMA有一个baseDMA引用参数,基类引用可以指向派生类型,因此,baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。 ```cpp hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) { style = new char[std::strlen(hs.style) + 1]; std::strcpy(style, hs.style); } ``` 3. 赋值运算符 显式调用基类赋值运算符`baseDMA::operator=(hs)`。 **派生类如何使用基类的友元?** 使用基类的;强制类型转换 - 阅读全文 -
CPP 12. 类和动态内存分配 2020-11-11 浏览量 716 暂无评论 # 第12章 类和动态内存分配 ## 12.1 动态内存和类 想在类中使用new和delete运算符,将会比平时多出许多问题,在这种情况下,析构函数必不可少。 🐸小知识: 1. 静态类成员特点:无论创建了多少对象,程序都只创建一个静态类变量副本; 2. 可以在类声明之外使用单独的语句初始化静态类成员; ```cpp // stringbad.h #include #ifndef STRINGBAD_H_ #define STRINGBAD_H_ class StringBad { private: char * str; int len; static int num_strings; public: StringBad(const char * s); StringBad(); ~StringBad(); friend std::ostream & operator<<(std::ostream & os, const StringBad & st); }; #endif // stringbad.cpp #include #include "stringbad.h" using std::cout; int StringBad::num_strings = 0; StringBad::StringBad(const char * s) { len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); num_strings++; cout << num_strings << ": \"" << str << "\" object created\n"; } StringBad::StringBad() { len = 4; str = new char[4]; std::strcpy(str, "C++"); num_strings++; cout << num_strings << ": \"" << str << "\" default object created\n"; } StringBad::~StringBad() { cout << "\"" << str "\" object deleted, "; --num_strings; cout << num_strings << " left\n"; delete [] str; } std::ostream & operator<<(std::ostream & os, const StringBad & st) { os << st.str; return os; } ``` ## 12.2 构造函数中使用new的注意事项 - 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete; - new和delete必须相互兼容,new对应于delete,new[]对应于delete[]; - 如果有多个构造函数,则必须以相同的方式使用new,因为析构函数只有一个; - 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象; ```cpp String::String(const String & st) { num_strings++; len = st.len; str = new char [len + 1]; std::strcpy(str, st.str); } ``` - 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象; ```cpp String & String::operator=(const String & st) { if (this == &st) return *this; delete [] str; len = st.len; std::strcpy(str, st.str); return *this; } ``` 🌈小提示: 到目前为止,涉及的都是在一个类内,那如果一个类包含其他类呢?这些函数要怎么变? **第一种情况**,类中没有其他需要复制构造函数和赋值运算符的成员: ```cpp // String and string class Magazine { private: String title; string publisher; } ``` 这个例子中,不需要为Magazine类编写复制构造函数和赋值运算符,因为逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。 **第二种情况**,类中有其他需要复制构造函数和赋值运算符的成员: 这些函数必须显式地调用String和string的复制构造函数和赋值运算符。 ## 12.3 有关返回对象的说明 - 返回对象将调用复制构造函数,而返回引用不会; - 如果被返回的对象是被调用函数中的局部变量,则应返回对象; - 返回const对象有助于防止将其作为左值; - 如果方法或函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个纸箱这种对象的引用; ## 12.4 指针和对象 - 使用常规表示法来声明指向对象的指针; - 可以将指针初始化为指向已有对象; - 可以使用new来初始化指针,这将创建一个新的对象; - 对类使用new将调用相应的类构造函数来初始化新创建的对象; - 可以使用->运算符通过指针访问类方法; - 可以对对象指针应用解除引用运算符(*)来获得对象; - **delete不能与定位new运算符配合使用**(挺难的),需要显式地调用析构函数 ```cpp #include #include #include using namespace std; const int BUF = 512; class JustTesting { private: string words; int number; public: JustTesting(const string & s = "Just Testing", int n = 0) {words = s; number = n; cout << words << " constructed\n";} ~JustTesting() {cout << words << " destroyed\n";} void Show() const {cout << words << ", " << number << endl;} }; int main() { char * buffer = new char[BUF]; JustTesting *pc1, pc2; pc1 = new (buffer) JustTrsting; pc2 = new JustTesting("Heap1", 20); cout << "Memory block addressed:\n" << "buffer: " << (void *) buffer << " heap: " << pc2 << endl; cout << "Memory contents:\n"; cout << pc1 << ": "; pc1->Show(); cout << pc2 << ": "; pc2->Show(); JustTesting *pc3, *pc4; // 偏移量sizeof(JustTesting) pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6); pc4 = new JustTesting("Heap2", 10); cout << "Memory contents:\n"; cout << pc3 << ": "; pc1->Show(); cout << pc4 << ": "; pc2->Show(); delete pc2; delete pc4; pc3->~JustTesting(); pc4->~JustTesting(); delete [] buffer; cout << "Done\n"; return 0; } ``` ## 12.5 队列 🐸小知识: 1. 可以对常量进行初始化,但不能给它赋值,因此,对于const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化,C++可以用成员初始化列表完成这项工作,它并不限于初始化常量; 2. 被声明为引用的类成员同上; ```cpp // 成员初始化列表语法,这种格式只适用于构造函数 Classy::Classy(int n, int m) : mem1(n), mem2(0), mem3(n*m + 2) { //... } // 这个实现方法无法正常运行 Queue::Queue(int qs) { front = rear = NULL; item = 0; qsize = qs; } // 这个可以 Queue::Queue(int qs) : qsize(qs) { front = rear = NULL; item = 0; } ``` 先进先出(FIFO) ```cpp class Queue { private: struct Node {Item item; struct Node * next;} enum {Q_SIZE = 10}; Node * front; Node * rear; int items; const int qsize; public: Queue(int qs = Q_SIZE); ~Queue(); bool isempty() const; bool isfull() const; int queuecount() const; bool enqueue(const Item &item); bool dequeue(Item &item); }; ``` - 阅读全文 -
CPP 11. 使用类 2020-11-09 浏览量 713 暂无评论 # 第11章 使用类 ## 11.1 运算符重载 运算符重载是一种形式的C++多态,可使代码看起来更自然,隐藏内部机理,强调实质。 ```cpp operatorop(argument-list) // 重载+运算符 operator+() ``` 🌈一些限制: 1. 重载后的运算符必须至少有一个操作数是用户定义的类型,防止用户为标准类型重载运算符; 2. 使用运算符时不能违反操作符原来的句法规则,包括优先级; 3. **不能创建新运算符**; 4. 不能重载下面的运算符: - sizeof - . - .* - :: - ?: - typeid - 强制类型转换运算符const_cast, dynamic_cast, reinterpret_cast, static_cast 5. 下面的运算符只能使用成员函数重载 - = - () - [] - -> ## 11.2 友元 🐸小知识: 1. 友元有三种:友元函数、友元类、友元成员函数。 2. **通过让函数成员友元函数,可以赋予该函数与类的成员函数相同的访问权限。** 在为类重载二元运算符时(操作数类型不一样)常常需要友元,以实现交换顺序也能运算,而不是必须左侧操作数为调用对象。 3. **注意友元函数不在类作用域内,因此使用类成员时要加限定。** **创建友元函数:把原型放在类声明中,并在前面加上关键字friend,但定义时不需要加friend,也不需要加限定符** ```cpp Time operator*(double n) const; A = B * 2.75; 将被转换为下面的成员函数调用 A = B.operator*(2.75); 那A = 2.75 * B怎么办呢?这时候友元函数就派上用场了 friend Time operator*(double m, const Time & t); 意味着以下两点: 1. 它不是成员函数; 2. 它与成员函数的访问权限相同。 下面的两个原型将冲突 Time operator+(const Time & t) const; friend Time operator+(const Time & t1, const Time & t2); ``` **运算符<<重载** ```cpp 1. 第一种 void operator<<(ostream & os, const Time & t) { os << t.hours << " hours, " << t.minutes << " minutes"; } 2. 第二种 ostream & operator<<(ostream & os, const Time & t) { os << t.hours << " hours, " << t.minutes << " minutes"; return os; } 第一种重载不可以使用`cout << "Trip time: " << trip << " (Tuesday)\n";` 第二种重载就可以,因为它返回的是ostream对象的引用。 ``` ## 11.3 类的自动转换和强制类型转换 🐸小知识: 1. 只有接受一个参数的构造函数才能作为转换函数,如果是多个参数,但第二个以后都提供默认值也可以; 2. 只有接受一个参数的构造函数可以将类型与该参数相同的值转换为类; 3. 构造函数只用于从某种类型到类类型的转换,要进行相反的转换,需要使用**转换函数**; 4. 默认的复制构造函数和赋值运算符为浅复制,即对成员进行逐个复制,可能导致数据受损; 5. delete[]与使用new[]初始化的指针和空指针`str = 0; str = nullptr;`兼容 **构造函数类型转换** ```cpp Stonewt(double lbs); Stonewt myCat; myCat = 19.6; // 如果声明了要显式,则该语句无效 mycat = Stonewt(19.6); mycat = (Stonewt) 19.6; 程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值,随后,采用逐成员赋值方式将该临时对象的内容复制到mtCar中,这一过程叫隐式转换。 可以使用explicit关闭自动隐式转换。(谨慎地使用隐式转换) explicit Stonewt(double lbs); ``` **转换函数类型转换** ```cpp 声明 operator typeName() const; 定义 className::operator typeName() const { return typeName (xxx) } ``` **复制构造函数** 用于将一个对象复制到新创建的对象中。 **每当程序生成了对象副本时,编译器都将使用复制构造函数。** ```cpp 原型 Class_name(const Class_name &); 何时调用? 新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用 假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数: StringBad ditto(motto); StringBad metto = motto; StringBad also = StringBad(motto); StringBad * pStringBad = new StringBad(motto); ``` **赋值运算符** 将已有的对象赋给另一个对象时,将使用重载的赋值运算符。 ```cpp StringBad headline1("Jineng Han"); StringBad knot; knot = headline1; ``` - 阅读全文 -
CPP 10. 对象和类 2020-11-07 浏览量 670 暂无评论 # 第10章 对象和类 ## 目录 - [过程性编程和面向对象编程](#过程性编程和面向对象编程) - [类概念]() - [如何定义和实现类]() - [公有类访问和私有类访问]() - [类的数据成员]() - [类方法(类函数成员)]() - [创建和使用类对象]() - [类的构造函数和析构函数]() - [const成员函数]() - [this指针]() - [创建对象数组]() - [类作用域]() - [抽象数据类型]() ## 10.1 过程性编程和面向对象编程 过程性编程首先考虑要遵循的步骤,然后考虑如何表示数据;OOP首先考虑数据--如何表示数据、如何使用数据。 ## 10.2 抽象和类 类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个简洁的包。 🐸小知识: 1. 使用同一个类所创建的不同对象都有自己的存储空间,用于存储内部变量和类成员,但是同一个类的所有对象共享同一组类方法; **定义类** 类规范由两部分组成: 1. 类声明:以数据成员的方式描述数据部分,以成员函数的方式描述公有接口;(蓝图) 2. 类方法定义:描述如何实现类成员函数。(细节) ```cpp // 通常将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中 // stock.h -- Stock class interface // verson 00 #ifndef STOCK00_H_ #define STOCK00_H_ #include 公有成员函数是程序和对象的私有成员之间的桥梁,程序不能直接访问私有数据,这被称为数据隐藏 C++中包括了许多专门用来实现OOP方法的特性,例如 1. 将数据表示和函数原型放在一个类声明中,来使描述成为一个整体, 2. 隐藏数据是OOP主要的目标之一--让数据表示成为私有,使得数据只能被授权的函数访问 class Stock { // private可以省略,默认的 private: std::string company; long shares; double share_val; double total_val; // 定义位于类声明中的函数将自动成为内联函数 void set_tot() {total_val = shares * share_val;} public: void } ``` ## 10.3 类的构造函数和析构函数 作用是让使用类对象能像使用标准类型一样,比如可以初始化。 ### 10.3.1 构造函数 类构造函数专门用于构造新对象、将值赋给它们的数据成员,C++已经为这些成员函数提供了名称和使用语法,需要程序员提供方法定义,程序声明对象时,将自动调用构造函数。 🐸小知识: 1. 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数,为类定义了构造函数后,程序员就必须为它提供默认构造函数; 2. 构造函数的声明和定义 ```cpp Stock(const string & co, long n = 0, double pr = 0.0); 注意,这里的形参不能和类数据成员的名称相同,可以在数据成员名中使用m_前缀或者_后缀 Stock::Stock(const string & co, long n, double pr) { company = co; if (n < 0) { std::cerr << "Number of shares can't be megative; " << company << " shares set to 0.\n"; shares = 0 } else shares = n; share_val = pr; set_tot(); } // 默认构造函数,没有任何参数 Stock::Stock() { company = "no name"; shares = 0; share_val = 0.0; total_val = 0.0; } // 显式 Stock food = Stock("World Cabbage", 250, 1.25); // 隐式 Stock food("World Cabbage", 250, 1.25); Stock food {"World Cabbage", 250, 1.25}; Stock food = {"World Cabbage", 250, 1.25}; ``` ### 10.3.2 析构函数 析构函数完成清理工作,在类对象过期时自动被调用。 如果构造函数使用了new,则必须提供使用delete的析构函数。 ```cpp // 原型 ~Stock(); // 定义 Stock::~Stock() { cout << "Bye, " << company << "!\n"; } ``` ### 10.3.3 const成员函数 const对象不能调用非const成员函数,声明和定义const成员函数 ```cpp void show() const; void stock::show() const ``` ## 10.4 this指针 刚才的每个类成员函数都只涉及一个对象,但有时候方法可能涉及两个对象,这时候就需要用到C++的this指针,它指向用来调用成员函数的对象。 ```cpp const Stock & Stock::topval(const Stock & s) const { if (s.total_val > total_val) return s; else return *this; } ``` ## 10.5 对象数组 初始化对象数组的方案:首先使用默认构造函数创建数组元素,然后使用花括号中的构造函数创建临时对象,然后将临时对象的内容复制到响应的元素中。**因此,要创建类对象数组,这个类必须有默认构造函数。** ```cpp Stock mystuff[4]; const int STKS = 4; Stock stocks[STKS] = { Stock("A", 12.5, 20), Stock(); Stock("B", 10.2, 20) }; 只初始化了3个,余下的1个使用默认构造函数进行初始化 ``` ## 10.6 类作用域 在类中定义的名称(如类数据成员和类成员函数名)的作用域都为整个类,只在该类中是一直的,在类外不可知,因此,不同类中的同名成员是不同的。 **作用域为类的常量** ```cpp class Bakery { private: const int Months = 12; // 这是不对的,因为声明类只是描述了对象的形式 double costs[Months]; } 1. 可以使用枚举来实现 class Bakery { private: enum {Months = 12}; double costs[Months]; } 2. 使用static关键字 class Bakery { private: static const int Months = 12; double costs[Months]; } ``` ## 10.7 抽象数据类型 ### 10.7.1栈 - 可创建空栈 - 可将数据项添加到堆顶 - 可从栈顶删除数据项 - 可查看栈是否填满 - 可查看栈是否为空 ```cpp // stack.h #ifndef STACK_H_ #define STACK_H_ typedef unsigned long Item; class Stack { private: enum {MAX = 10}; Item items[MAX]; int top; public: Stack(); bool isempty() const; bool isfull() const; bool push(const Item & item); bool pop(Item & item); }l #endif ``` - 阅读全文 -