CPP 12. 类和动态内存分配 2020-11-11 浏览量 576 暂无评论 # 第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 浏览量 568 暂无评论 # 第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 浏览量 527 暂无评论 # 第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 ``` - 阅读全文 -
CPP 9. 内存模型和名称空间 2020-11-05 浏览量 562 暂无评论 # 第9章 内存模型和名称空间 ## 9.1 单独编译 一般将一个大型文件分解为三部分: - 头文件:包含结构声明和使用这些结构的函数的原型(通常有函数原型、使用#define或const定义的符号常量、结构声明、类声明、模板声明、内联函数,不能将函数定义放在头文件中,当同一个程序的两个文件都包含了这个头文件时,相当于包含了同个函数的两次定义,将出错); - 源代码文件:包含与结构有关的函数的代码; - 源代码文件:包含调用与结构相关的函数的代码。 🐸小知识: 1. 在包含自己的头文件时,应使用引号而不是尖括号(首先查找当前的工作目录或源代码目录,而不是在标准位置); **示例** ```cpp // coordin.h // 使用ifndef避免在同一个文件中包含同一个头文件多次 // (当包含的文件中也包含该头文件时会出现这种情况) #ifndef COORDIN_H_ #define COORDIN_H_ struct polar { double distance; double angle; }; struct rect { double x; double y; }; polar rect_to_polat(rect xypos); void show_polar(polar dapos); #endif // file1.cpp #include #include "coordin.h" using namespace std; int main() { rect rplace; polor pplace; cout << "Enter the x and y values: "; while (cin >> rplace.x >> rplace.y) { pplace = rect_to_polar(repalce); show_polar(pplace); cout << "Next two numbers (q to quit): "; } cout << "Bye!\n"; return 0; } // file2.cpp #include #include #include "coordin.h" polar rect_to_polar(rect xypos) { using namespace std; polar answer; answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y); answer.angle = atan2(xypos.y, xypos.x); return answer; } void show_polar(polar dapos) { using namespace std; const double Rad_to_deg = 57.29577951; cout << "distance = " << dapos.distance; cout << ", angle = " << dapos.angle * Rad_to_deg; cout << " degrees\n"; } ``` ## 9.2 存储持续性、作用域和链接性 **存储持续性:** 自动存储持续性、静态存储持续性、线程存储持续性(C++11)、动态存储持续性。 **作用域(scope):** 名称在文件的多大范围内可见,分为局部和全局(名称空间作用域的特例)。 **链接性(linkage):** 名称如何在不同文件间共享,分为无、外部和内部。 ```cpp int global = 1000; // 外部链接,程序的其他文件中也可以使用 static int one_file = 50; // 内部链接,只在该文件中可以使用; int main() { ... } void funct1(int n) { static int count = 0; // 无链接静态变量,只在该函数中能访问,但在程序整个运行期间都在内存中 int llama = 0; // 自动变量 ... } ``` 🐸小知识: 1. 默认情况下,函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性,存储在栈中,后进先出; 2. 关键字auto和register的演变历史(之前的用法现在不用了,但不非法); 3. 虽然无链接性的静态持续变量和自动变量都是只能在当前函数或代码块中访问,但是自动变量不会像静态持续变量那样在整个程序执行期间存在,适用于再生; 4. 单定义规则:如果要在多个文件中使用**外部变量**,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它; 5. 作用域解析运算符"::",放在变量名前面表示使用变量的全局版本; 6. 虽然使用全局变量可以不用传递参数,但这样的程序不可靠,通常情况下,应使用局部变量,外部存储适于表示常量数据; 7. const全局变量的链接性为内部; ```cpp // 第一个const防止字符串被修改,第二个字符串防止每个指针的位置被修改 const char * const months[12] = { "January", "February", "March", ..., "December" } ``` 8. 一个文件中与另一文件中常规外部变量的同名静态外部变量将隐藏常规外部变量; ### 9.2.1. 说明符和限定符 **a. c-v限定符** - const - volatile。表明即使程序代码没有对内存单元进行修改,其值也可能发生变化,比如串口通信中,硬件可能修改其中的内容。它的作用是改善编译器的优化能力,防止这种变量被错误优化。 **b. mutable** 用来指出即使结构(或类)变量为const,其某个成员也可以被修改。 ### 9.2.2. 函数的链接性 1. 默认情况下,函数的链接性为外部,可以在不同的文件中使用;还可以用关键字static将函数的链接性设置为内部的,在定义静态函数的文件中,静态函数将覆盖外部定义。 2. 单定义规则也适用于非内联函数,这意味着**在多文件程序中,只能有一个文件包含该函数的定义(该文件可能是库文件)**; 3. 内联函数的定义可以放在头文件中; 4. C++查找函数的顺序:(若静态)本文件-->所有程序文件-->库; ### 9.2.3. 语言链接性 C语言和C++编译器为函数名重命名的方式不同,因此在C++中想要使用C库文件中预编译的函数时,需要指定使用C语言链接性。 ```cpp extern "C" void spiff(int); // C extern void spoff(int); // C++ extern "C++" void spaff(int); // C++ ``` ## 9.3 名称空间 名称空间是为了解决大项目中的名称冲突而出现的。 ```cpp namespace A { double pail; void fetch(); } // 名称空间是开放的,即可以把名称加入到已有的名称空间中 namespace A { int haha; } namespace B { int pail; double featch; } // 名称空间可以嵌套 namespace C { namespace D { int a; ... } float water; } // 可以给名称空间创建别名 namespace ABC = A::B::C; // 限定的名称 A::haha b::pail // 如果不想每次使用都进行限定怎么办呢?两种办法 // 1. using声明,使特定的标识符可用 using A::haha; // 2. using编译指令,使整个名称空间可用 using namespace A; #include // 将名称加入名称空间std using namespace std; // 使名称全局可用 ``` ## 9.4 总结 尽量将变量、函数库、类库写在名称空间中,多方便,多优雅。 - 阅读全文 -
CPP 8. 函数探幽 2020-11-03 浏览量 574 暂无评论 # 第8章 函数探幽 ## 8.1 C++内联函数 🐸小知识: 1. 内联函数是C++为提高程序运行速度所做的一项改进,具体是怎么提高运行速度的呢? **调用常规函数:** 存储调用指令的地址-->复制函数参数至堆栈-->跳到标记函数起点的内存单元并执行函数代码(也许还需将返回值放入到寄存器中)-->调回地址被保存的调用指令处。 **使用内联函数:** 编译器使用相应的函数代码替换函数调用,无需跳来跳去,以占用更多内存为代价。 2. 内联函数不能递归; 3. 使用规则:要么在函数声明前加上关键字inline,要么在函数定义前加上关键字inline,通 常省略原型,将整个定义放在本应提供原型的地方; 4. 大函数不适合用内联,因为本身执行时间就很长; 5. C++内联函数可以按值传递,但C语言的宏定义`#define SQUARE(X) ((X)*(X))`不可以按值传递; **示例:** ```cpp #include inline double square(double x) {return x * x;} int main() { using namespace std; double a, b; double c = 13.0; a = square(5.0); b = square(4.5 + 7.5); cout << "a = " << a << ", b = " << b << endl; cout << "c = " << c; cout << ", c square = " << square(c++) << endl; } ``` ## 8.2 引用变量 引用是一种复合类型,它是已定义的变量的别名,与原变量指向相同的值和地址; 🐸小知识: 1. 通过将引用变量用作参数,函数将使用原始数据,而不是其副本; 2. 引用必须在声明时将其初始化,更接近const指针; 3. 在用作函数参数时,尽可能使用const; 4. 返回引用时直接把返回值复制给接收变量,而不用创建临时变量(应避免返回临时变量的引 用或指针); 5. 返回引用的函数实际上式被引用的变量的别名; **创建引用变量** ```cpp int rats; int & rodents = rats; // 让rodents成为rats的别名 ``` **将引用用作函数参数** ```cpp #include void swap(int & a, int & b); int main() { using namespace std; int wallet1 = 300; int wallet2 = 350; swap(wallet1, wallet2); } void swap(int & a, int & b) { int temp; temp = a; a = b; b = temp; } ``` **临时变量、引用和const** C++将在两种情况下生成临时变量(仅当参数为const时,C++才允许这样): 1. 实参的类型正确,但不是左值; 2. 实参的类型不正确,但可以转换为正确的类型。 临时变量并不会影响原变量。 ```cpp double refcube(const double &ra) { return ra * ra * ra; } double side = 3.0; double * pd = &side; double & rd = side; long edge = 5L; double lens[4] = {2.0, 5.0, 10.0, 12.0}; double c1 = refcube(side); double c2 = refcube(lens[2]); double c3 = refcube(rd); double c4 = refcube(*pd); double c5 = refcube(edge) // ra is temporary variable, case 2 double c6 = refcube(7.0); // ra is temporary variable, case 1 double c7 = refcube(side + 10.0); // ra is temporary variable, case 1 ``` **右值引用** 这种引用可指向右值,使用&&声明。 ```cpp double && rref = std::sqrt(36.00); double j = 15.0; double && jref = 2.0 * j * 18.5; std::out << rred << endl; std::out << jred << endl; ``` **将引用作为函数的返回值** ```cpp // 返回string const string v1(const string & s1, const string & s2) { string temp; string = s2 + s1 +s2; return temp; } // 返回string的引用 const string & v2(string & s1, const string & s2) { s1 = s2 + s1 + s2; return s1; } ``` **对象、继承和引用** 1. 派生类继承基类的方法; 2. 基类引用可以指向派生类对象。 注意下面的程序:参数os(类型为ostream &)可以指向ostream对象(如cout),也可以指向ostream的派生类ofstream对象(如fout)。 ```cpp #include #include #include using namespace std; void file_it(ostream & os, double fo, const double fe[], int n); const int LIMIT = 5; int main() { ofstream fout; const char * fn = "ep-data.txt"; fout.open(fn); if (!fout.is_open()) { cout << "Can't open " << fn << ". Bye.\n"; exit(EXIT_FAILURE); } double objective; cout << "Enter the focal length of your " "telescope objective in mm: "; cin >> objective; double eps[LIMIT]; cout << "Enter the focal lengths, in mm, of " << LIMIT << " eyepieces: \n"; for (int i = 0; i < LIMIT; i++) { cout << "Eyepiece #" << i + 1 << ": "; cin >> eps[i]; } file_it(fout, objective, eps, LIMIT); file_it(cout, objective, eps, LIMIT); cout << "Done\n"; cin.get(); cin.get(); return 0; } void file_it(ostream & os, double fo, const double fe[], int n) { ios_base::fmtflags initial; initial = os.setf(ios_base::fixed); os.precision(0); os << "Focal length of objective: " << fo << " mm\n"; os.setf(ios::showpoint); os.precision(1); os.width(12); os << "f.1. eyepiece"; os.width(15); os << "magnification" << endl; for (int i = 0; i < n; i++) { os.width(12); os << fe[i]; os.width(15); os << int (fo/fe[i] + 0.5) << endl; } os.setf(initial); } ``` **何时使用引用参数:** 1. 修改调用函数中的数据对象; 2. 提高程序的运行速度; ## 8.3 默认参数 🐸小知识: 1. 通过函数原型设置默认值`char * left(const char * str, int n = 1);`,必须从右向左添加默认值; 2. ***在设计类时通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量;*** ## 8.4 函数重载(函数多态) C++在C语言的基础上新增的功能。 特征标:函数的参数列表,包括参数数目、类型、排列顺序。 🐸小知识: 1. 默认参数允许使用不同数目的参数调用同一个函数,而函数多态允许使用多个同名的函数(**那C++怎么知道是在用同一个函数的默认参数还是多态的同名的函数呢?函数重载的条件是它们的特征标不同**); 2. 从编译器的角度考虑特征标; 3. 没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配,但如果有多个函数满足强制匹配,则C++将其视为错误; 4. 匹配函数时,并不区分const和非const变量; ```cpp void dribble(char * bits); void dribble(const char *cbits); void dabble(char * bits); void drivel(const chat * bits); const char p1[20] = "How's the weather?"; char p2[20] = "How are you?"; dribble(p1); // dribble(const char *) dribble(p2); // dribble(char *) dabble(p1); // no match dabble(p2); // dabble(char *) drivel(p1); // drivel(const char *) drivel(p2); // drivel(const char *) ``` 5. 重载引用参数时,将调用最匹配的版本(参数是左值、const还是右值); 6. C++怎么分得清到底调用哪个 **何时使用函数重载** 仅当函数基本上执行相同的任务,但使用不同类型的参数时,才应使用函数重载,如果只是参数数量不同,可以考虑默认参数。 ## 8.5 函数模板 函数模板就是使用泛型来定义函数,其中的泛型可以用具体的类型来替换,编译器在编译的时候会将泛型替换为具体的类型。 🐸小知识: 1. 并非所有的模板参数都必须是模板参数类型; 2. 在原型声明时需要加上`template `; 3. 函数模板虽然试图使用泛型来简化代码,但是它仍然不能处理所有数据类型,比如常规变量 的`a=b`不能用于数组,`a*b`不能用于指针参数等,所以有必要使用不同的原型,那怎么分清使用哪个原型呢?这时候就要涉及到原型优先级:非模板函数>具体化>常规模板(见下面示例); 4. 既然可以隐式实例化,显式实例化有什么用呢?加快运行速度? ```cpp template // template void Swap(AnyType &a, AnyType &b) { AnyType temp; temp = a; a = b; b = temp; } ``` **具体化** ```cpp // non template function prototype, job is a struct void Swap(job &, job &); // template prototype template void Swap(T &, T &); // explicit specialization for the job type template <> void Swap(job &, job &); template <> void Swap(job &, job &); ``` **原型优先级** ```cpp #include using namespace std; void Swap(int &a, int &b); // 隐式实例化(explicit instantiation) template void Swap(T &a, T &b); // 显式实例化(explicit instantiation) template void Swap(int, int); // 显示具体化(explicit specialization),不能和显示实例化同时使用 template <> void Swap(job &j1, job &j2); void Show(job &j); struct job { char name[40]; double salary; int floor; }; int main() { cout.precision(2); cout.setf(ios::fixed, ios::floatfield); int i = 10, j = 20; cout << "i, j = " << i << ", " << j << ".\n"; cout << "Using compiler-generated int swapper:\n"; Swap(i, j); cout << "Now i, j = " << i << ", " << j << ".\n"; job sue = {"jhan", 4235454.76, 7}; job sidney = {"xyhan", 32424.56, 9}; cout << "Before job swapping:\n"; Show(sue); Show(sidney); Swap(sue, sidney); cout << "After job swapping:\n"; Show(sue); Show(sidney); cin.get(); return 0; } void Swap(int &a, int &b) { cout << "Enter regular Swap.\n"; int temp; temp = a; a = b; b = temp; } template void Swap(T &a, T &b) { cout << "Enter template Swap.\n"; T temp; temp = a; a = b; b = temp; } template <> void Swap(job &j1, job &j2) { cout << "Enter specification Swap.\n"; double t1; int t2; t1 = j1.salary; j1.salary = j2.salary; j2.salary = t1; t2 = j1.floor; j1.floor = j2.floor; j2.floor = t2; } void Show(job &j) { using namespace std; cout << j.name << " : $" << j.salary << " on floor " << j.floor << endl; } 输出: i, j = 10, 20. Using compiler-generated int swapper: Enter regular Swap. Now i, j = 20, 10. Before job swapping: jhan : $4235454.76 on floor 7 xyhan : $32424.56 on floor 9 Enter specification Swap. After job swapping: jhan : $32424.56 on floor 9 xyhan : $4235454.76 on floor 7 ``` **🌈原型的匹配及完全匹配(难点):** 在函数重载、函数模板、函数模板重载时,C++按以下过程决定使用哪个函数: 1. 创建候选函数列表。这里放名称一样的函数和模板函数; 2. 使用候选函数列表创建可行函数列表。这里是参数数目正确的函数; 3. 确定是否有最佳的可行函数。如果有,就使用它,否则调用出错。 从最佳到最差的顺序如下: 1. 完全匹配,但常规函数优先于模板函数; 2. 提升转换(如char和shorts自动转换为int,float自动转换为double); 3. 标准转换(如int转换为char,long转换为double); 4. 用户定义的转换,如类声明中定义的转换。 原型的完全匹配允许一些无关紧要的转换,如下表: 从实参|到形参 ---|--- Type|Type & Type &|Type Type[]|* Type Type (argument-list)|Type (*) (argument-list) Type|const Type Type|volatile Type Type *|const Type Type *|volatile Type * ```cpp #include template T lesser(T a, T b) { return a < b ? a : b; } int lesser (int a, int b) { a = a < 0 ? -a : a; b = b < 0 ? -b : b; return a < b ? a : b; } int main() { using namespace std; int m = 20; int n = -30; double x = 15.5; double y = 25.9; cout << lesser(m, n) << endl; // use #2 cout << lesser(x, y) << endl; // use #1 with double // 指出应使用模板函数 cout << lesser<>(m, n) << endl; // use #1 with int // 进行显示实例化 cout << lesser(x, y) << endl; // use #1 with int return 0; } ``` **关键字decltype和后置返回类型(C++11)** 在C++98中,有时无法确定编写的模板函数中用到的中间变量类型,这时候decltype就派上了用场(像numpy中的astype)。 ```cpp int x; decltype(x) y; decltype(x + y) xpy = x + y; double x = 5.5; decltype ((xx)) r = xx; // r2 is double & decltype (xx) w = xx; // w is double auto h(int x, float y) -> double; template auto gt(T1 x, T2 y) -> decltype(x + y) { ... return x + y; } ``` - 阅读全文 -