1. Introduction to Objects
public、private、protected
- public: 公开,都可以访问和使用
- private: 除对象内的函数之外都不能访问和使用
- protected: 对象内的和其继承类内的函数可以访问,其他不行
class P
{
private:
int data = 1;
void add(P a);
public:
void add2(int i)
{
data += i;
}
};
void P::add(P a)
{
data += a.data;
} // a是一个P类的对象,而add是P类的函数,因此可以通过a.data直接调用a的data
class Q
{
private:
void add(P a)
{
data += a.data;
} // a是一个P类的对象,但这里的add是Q类的函数,因此不能通过a.data直接调用a的data
public:
int data = 2;
};
int main()
{
P a, b; Q c;
int d = c.data; // c是Q类的对象,data是public定义的,可以随便调用
a.add(b); // P中的add是private的,类外不能直接调用
a.add2(d); // add2是public的,可以用
return 0;
}
inheritance & composition
- 继承:克隆现有的类,然后对其进行修改
- 组合:直接使用类的对象
Polymorphism(多态性)
- 早期(静态)绑定:在程序运行之前(由编译器和连接器)执行绑定
- 后期(动态)绑定:在运行时进行的绑定
多态性通过使用关键字virtual
来声明一个具有灵活后期绑定的函数来实现。
2. Making & Using Objects
compile -> link -> run
源文件:main.cpp、func.cpp
对象文件:main.o、func.o
可执行程序:test
Declarations vs. definitions
- declaration: 声明会向编辑器引入一个名称或标识符
int ADD(int a, int b);
int ADD(int, int);
- definition: 定义会为该名称分配一个空间
int ADD(int a, int b) {return a + b;}
同一个名称可以被声明多次,但只能被定义一次。
一般来说,在c++中,在定义一个变量的同时,该变量就会被初始化。
关键字extern
表示名称只是一个声明,并且该名称的定义在文件的外部,或者出现在文件的后面。
extern int x;
extern int arr[10];
3. The C in C++
Pointer & Reference
- pointer
一个指针可以保存一个地址。
定义指针时,必须声明其指向的变量的类型,并将其初始化。
c++可以将任何类型的指针赋值给一个空指针void*
,但不能将一个空指针赋值给其他类型的指针。
int i = 10;
int* p = &i;
void* vp = p; //ok
int* ip = vp; //error
int* ip = (int*)vp; //ok
- reference
一个引用&
必须在创建时就被初始化。
一旦将引用初始化为一个对象,就不能将其更改为引用另一个对象(指针可以指向另一个对象)。
不能有空的引用,即每一个引用都必须对应一个合法的存储地址。
int a = 3, b = 5;
int& m = a; // ok
int& m; // error
int& m = b; // error
int& k = m; // k引用m,即k也是引用a
int n = m; // m引用a,所以n的值为3
int* p = &m; // p是一个指针,指向m,即指向a
int*& ref = p;
//先考虑离得最近的符号,ref是一个引用,引用的是一个int型的指针
//这里是ref引用p
m = m + 5; // 最后a的值被修改为8
Pointer & Array
数组的名称可以用作指向其最初元素的指针。
数组的访问可以通过一个指向数组的指针加索引或者是一个指向元素的指针来实现。
int v[] = {1, 2, 3, 4};
int* p1 = v;
int* p2 = &v[2];
// v[2], *(v+2), *(p1+2), *p2都指向3
Pointer to data member
// C11: PointerToMemberData.cpp
#include <iostream>
using namespace std;
class Data
{
public:
int a, b, c;
void print() const
{
cout << a << "," << b << "," << c << endl;
}
};
void main( )
{
Data d;
Data* dp = &d; // 指向类的对象的指针
int Data::*pmInt = &Data::a; // 指向类内成员的指针
// 利用指针访问类的成员和成员函数
dp->*pmInt = 47;
pmInt = &Data::b;
d.*pmInt = 48;
pmInt = &Data::c;
dp->*pmInt = 49;
dp->print();
}
Pointer to member function
// C11: PointerToMemberFunction.cpp
#include <iostream>
using namespace std;
class Widget
{
public:
void f(int) const { cout << "Widget::f()\n"; }
void h(int) const { cout << "Widget::h()\n"; }
};
void main( )
{
Widget w;
Widget* wp = &w;
void (Widget::*pmem)(int) const = &Widget::h;
// 定义了一个void型指向Widget成员函数,其参数为一个int的函数指针
// 并将其赋值为h
(w.*pmem)(1); // 输出为 Widget::h()
(wp->*pmem)(2); // 输出为 Widget::h()
// 函数指针的使用方法
}
Argument passing
传递方式 | 按值传递 | 指针传递 | 引用传递 |
---|---|---|---|
传入的形式 | 变量 | 指针 | 引用 |
实际的传入 | 变量的值 | 地址 | 变量名 |
特征 | 变量的值传给形参 | 形参指向实际的变量 | 形参是实际变量的引用 |
作用 | 传入参数和实参是独立的 | 传入和实参同时被修改 | 传入和实参同时被修改 |
// C03: PassByValue.cpp
#include <iostream>
using namespace std;
void f(int a)
{
cout << "a = " << a << endl;
a = 5;
cout << "a = " << a << endl;
}
void main()
{
int x = 47;
cout << "x = " << x << endl;
f(x);
cout << "x = " << x << endl;
}
在f中,形参的值是按值传递,因此并不会修改实际参数的值,程序的打印结果为
x = 47
a = 47
a = 5
x = 47
// C03: PassReference.cpp
#include <iostream>
using namespace std;
void f(int& a)
{
cout << "a = " << a << endl;
a = 5;
cout << "a = " << a << endl;
}
void main()
{
int x = 47;
cout << "x = " << x << endl;
f(x);
cout << "x = " << x << endl;
}
在f中,传入的参数是实参的引用,因此在函数内对形参的修改也会同样作用到实际的参数上,程序的打印结果为
x = 47
a = 47
a = 5
x = 5
Function pointer
void (*funcPtr)( );
funcPtr
是指向一个没有参数和返回值的函数的指针。
void* funcPtr ( );
funcPtr
是一个函数,其返回值是一个void*
型的指针
void (*funcPtr)( ) = func;
函数名func
可以表示函数func()
的地址
4. Data Abstraction
Header file etiquette
在创建新的数据类型时,应将接口(声明)与实现(成员函数的定义)分开,这样可以在不重新编译整个系统的情况下实现更改。
我们在头文件中使用#include guards,以避免在一个编译单元的编译过程中多次#include了同一个头文件。例如
//main.cpp
#include "myfile1.h"
#include "myfile2.h"
...
//myfile1.h
#include "myhead.h"
...
//myfile2.h
#include "myhead.h"
...
在编译main.cpp的过程中,由于myfile1.h和myfile2.h中都有#include "myhead.h"
,导致头文件myhead.h被#include了两次。因此,可以在头文件中使用#include guards。
如
//myfile1.h
#ifndef MYHEAD_H
#define MYHEAD_H
#include "myhead.h"
#endif
...
//myfile2.h
#ifndef MYHEAD_H
#define MYHEAD_H
#include "myhead.h"
#endif
...
5. Hiding the Implementation
class Date // 关键字class,名称Date
{
public: // access specifier 存取说明符
// 成员函数
void SetDate(int y,int m,int d);
int IsLeapYear();
void Print();
private:
// 数据成员
int year,month,day;
}; // semicolon 注意分号
// member function’s definition
void Date::SetDate(int y, int m, int d){...}
friends & access control
友元可以是
- 一个全局函数
- 另一个类中的一个成员函数
- 一个类
友元可以访问一个类的私有部分。
友元是不对称的、不可被传递的、不可被继承的。
class A
{
int data;
friend void example(A &a);
};
void example(A &a)
{
cout << a.data << endl; // 可以访问一个类的私有部分
}
6. Initialization & Cleanup
Initialization with the constructor
- 在C++中,编译器在创建对象时自动调用构造函数。
- 构造函数与类具有相同的名称
- 构造函数可以被重载
- 构造函数没有返回值
Default constructors
默认构造函数是一个可以没有参数的构造函数。
class A
{
private:
int a;
double b;
public:
A() {} // default constructor
A(int i) { a = i; }
A(int i, double j) { a = i; b = j; }
};
ClassName a; // ok
ClassName b = ClassName(); // ok
ClassName c(); // error
Cleanup with the destructor
- 一个析构函数会清理并释放资源,在对象被销毁时会被调用
- 析构函数的识别方法是与类本身具有相同的名称和补体符号
~
- 析构函数没有参数,不能被重载,没有返回值
Initialization & Cleanup of Object Arrays
class Date
{
int year, month, day;
public:
Date()
{
year=month=day=0;
cout<<"Default constructor called."<<endl;
}
Date(int y, int m, int d)
{
year=y;
month=m;
day=d;
cout<<"Constructor called."<<day<<endl;
}
~Date() { cout<<"Destructor called."<<day<<endl; }
void Print( ){cout<<year<<":"<< month<<":"<<day<<endl; }
};
void main()
{
Date dates[3]={Date(2003,9,20), Date(2003,9,21)};
for (int i=0;i<3;i++)
{
dates[i].Print();
}
}
打印结果为
Constructor called.20
Constructor called.21
Default Constructor called.
2003:9:20
2003:9:21
0:0:0
Destructor called.0
Destructor called.21
Destructor called.20
7. Function Overloading
对不同类型的操作用相同的名称来命名,称为重载。
- 参数的数量不同
- 参数的类型不同
add(int a, int b);
add(int a, double b);
add(int a, int b, int c);
- 在函数重载中不能仅以返回值作区别
float f(int s) { return s / 2.0; }
int f(int s) { return s * 2; }
int main()
{
cout << f(3) << endl; // which f?
return 0;
}
- Notice: 内置类型转换
void print(int score)
{
cout << "int = " << score << endl;
}
void print(float score)
{
cout << "float = " << score << endl;
}
int main()
{
float a = 1.2;
print(a);
return 0;
}
其打印结果为
float = 1.2
如果将第二个print()
删去,则打印结果为
int = 1
- Notice: 缺省参数
- 带默认值的参数必须放在参数列表的末尾
int f(int a, int b=0, char*c =“pass”); // ok
int g(int a=0, int b, char*c =“pass”); // error
- 默认参数在同一个作用域中只能初始化一次
void fun(int x=7);
void fun(int x=8); // error: redefinition
- Notice: 带有默认参数的重载
void fun(int a, int b=1) {cout << a + b << endl; }
void fun(int a) {cout << a << endl; }
fun(2); // error
int fun(int a=1) { return a+1; }
float fun(float a) { return a; }
int fun(int a, int b) { return a+b; }
int main()
{
float a = 2.5;
int b = 2;
cout << fun(fun(a, b)) + fun(fun(a), b) << endl; // output is 9
return 0;
}
8. Constants
Value substitution
一个常量必须被初始化,并且不能修改常量的值。
const int model=90; // model is a const
const int v[]={1,2,3,4}; // v[i] is a const
const int x; // error: no initilizer
model=90; // error
v[2]++; // error
Pointer to const & Const pointer
const float f1 = 5.0; // 常量
float f2 = 0.0; // 变量
float* p1= &f1; // error
float* const p2 = &f1; // error
// 只有指向常量的指针才能指向常量,其他指针都只能指向变量
const float* p3 = &f1; // 指向常量的指针
float* p4 = &f2;
float* const p5 = &f2; // ok
// 常量指针指向的地址不能被修改
const float* p6 = &f2; // ok
Static data members
- 静态数据成员必须被初始化
- 在类外赋值
- 不用加
static
关键字 - 前面加上类名以限定
class Myclass
{
private:
static int obj; // static data member
};
int Myclass::obj=8; // initialization
Const data members
- 常量数据成员不能被修改
- 常量数据成员必须在构造函数中进行初始化
- 由于常量和引用不能被修改,因此一个成员中有常量或引用的类不能被默认构造。
class A
{
const int a; // const data member
static const int b; // static const
const int& r; // const reference
public:
A(int i);
void Print();
};
const int A::b=10;
A::A(int i): a(i),r(a) {...}
Const member functions
常成员函数的定义形式为
type function-name (arguments) const;
- 常成员函数不能修改除可变成员之外的数据成员的值
- 在类外定义常成员函数时,需要加上
const
后缀
Const objects
- 非常量对象既可以调用非常成员函数,也可以调用常成员函数
- 常量对象只能调用常成员函数,不能调用非常成员函数
常成员 | 非常成员 | |
---|---|---|
常量对象 | yes | no |
非常量对象 | yes | yes |
9. Inline Functions
- 内联函数必须在其被调用之前定义。
- 内联函数的函数体不包含迭代、切换和异常处理。
- 内联函数不能是递归的。
- 在类内定义内联函数时,关键字
inline
不是必须的。 - 内联函数必须短或简单。
- 如果函数太复杂,编译器无法执行内联操作。
- 如果函数的地址被隐式或显式地获取,编译器也无法执行内联操作。
10. Name Control
Static elements
- 关键字
static
有两个基本含义
- 每次调用函数时,该对象都是在一个特殊的静态数据区中创建的,而不是在栈上。这就是静态存储的概念。
static
控制名称的可见性,因此名称不能在翻译单元或类外被看到。
-
可见性:外部链接、内部链接
- 外部链接:文件作用域中的名称对于程序中所有的翻译单元(.cpp文件)都是可见的。全局变量和普通函数具有外部链接。
- 内部链接(file static):名称仅对其翻译单元可见。
static
、const
和inline
的名称默认为内部链接。
-
存储类型:静态数据区、栈、堆
- 静态数据区:
global
、static
- 栈:本地变量
- 堆:
new
、delete
- 静态数据区:
静态对象只会在程序开始时被构造,在程序结束时被析构,如
class X
{
int i;
public:
X(int ii = 0) : i(ii) { cout<<"X():"<<i<<endl;}
~X() { cout<<"~X():"<<i<<endl; }
};
void f()
{
static X x1(47);
static X x2;
}
void main( )
{
f();
f();
X x3(10);
}
由于x1、x2都是静态的,因此不会被反复构造和析构,打印输出为
X():47
X():0
X():10
~X():10
~X():0
~X():47
如果将static X x2;
的static
删去,则x2会因函数的调用和结束被重复构造和析构,打印输出为
X():47
X():0
~X():0
X():0
~X():0
X():10
~X():10
~X():47
Namespace
- 作用域解析操作符
::
- 使用指令:引入命名空间中的所有名称
namespace calculator
{
double Add(double x, double y) { return x + y; }
void Print(double x) { std::cout << x; }
}
using namespace calculator; // Using Directive
void main()
{
double a, b;
std::cin >> a >> b;
Print(Add(a, b));
}
不要在头文件中引入使用指令。
namespace calculator
{
double Add(double x, double y) { return x + y; }
void Print(double x) { std::cout << "Result is " << x; }
}
void Print(double x)
{
std:: cout << "This is an external function.";
}
using namespace calculator;
void main()
{
double a, b;
std:: cin >> a >> b;
Print(Add(a, b)); //ambiguous calling
}
在上述程序中,编译器无法判断调用的Print()
函数是哪一个。因此,在函数前面要加上作用域。如
calculator::Print(Add(a, b));
// output: Result is ...
::Print(Add(a, b));
// output: This is an external function.
- 使用声明:一次引入一个名称
namespace calculator
{
double Add(double x, double y) { return x + y; }
void Print(double x) { std::cout << "Result is " << x; }
}
void main()
{
using calculator::Add; // Using Declaration
double a, b;
std::cin >> a >> b;
calculator::Print(Add(a, b));
}
11. Copy Constructor
- 格式:
X::X(const X& )
- 拷贝构造函数的调用
- 用同一个类的一个已创建对象来初始化新创建的对象时调用
- 通过值传递参数时调用拷贝构造函数
- 当函数返回值是一个对象时调用拷贝构造函数
- 通过引用传递参数时不会调用拷贝构造函数,因为没有创建新对象
// tpoint.h
class TPoint
{
int X, Y;
public:
TPoint(int x,int y)
{
X=x;Y=y; cout<<"Constructor called."<<X<<endl;
}
TPoint(const TPoint& p); // copy constructor
~TPoint() { cout<<"Destructor called."<<X<<endl;}
int Xcoord() { return X; }
int Ycoord() { return Y; }
};
TPoint::TPoint(const TPoint& p)
{
X = p.X; Y=p.Y;
cout<< “Copy Constructor called.” <<X<<endl;
}
// tpoint.cpp
#include <iostream>
#include "tpoint.h"
using namespace std;
TPoint f(TPoint Q) // Pass by value
{
cout<<"OK! "<<endl;
int x, y;
x=Q.Xcoord()+10;
y=Q.Ycoord()+20;
TPoint R(x, y);
return R; // Return R’s data value
}
void main()
{
TPoint M(20,35), P(0,0);
TPoint N = M;
//M is a created object, N is a creating object
P = f(N);
cout<<"P="<<P.Xcoord()<<", "<<P.Ycoord()<<endl;
}
运行tpoint.cpp,打印输出为
Constructor called.20 // 创建 M
Constructor called.0 // 创建 P
Copy Constructor called.20 // 把 M 拷贝并赋值给 N
// 进入函数f
Copy Constructor called.20 // 把 N 拷贝给 Q (按值传递)
OK!
Constructor called.30 // 创建 R
Copy Constructor called.30 // 把 R 拷贝并作为返回值
Destructor called.30 // 析构 R
Destructor called.20 // 析构 Q
Destructor called.30 // 析构函数返回的临时对象
// 函数f结束
P=30, 55
Destructor called.20 // 析构 N
Destructor called.30 // 析构 P
Destructor called.20 // 析构 M
- 返回值优化
class A
{
int i;
public:
A(int x=0) {i=x; cout<<"Constructor."<<endl;}
A(const A& x){i=x.i; cout<<"Copy onstructor."<<endl;}
~A() {cout<<"Destructor."<<endl;}
};
A f() { return A(1);}
void main()
{
f();
cout<<"main() End."<<endl;
}
其打印结果为
Constructor.
Destructor.
main() End.
若将函数f()
改为
A f()
{
A a(1);
return a;
}
则打印结果会变成
Constructor.
Copy Constructor.
Destructor.
Destructor.
main() End.
12. Operator Overloading
- 使用
operator
关键字实现运算符重载,格式为
type operator @ (argument list)
{
// code
}
Member
- 格式
type operator @ (arguments) {...}
- 用法
a.operator @(b) // 二元运算符,a @ b
a.operator @() // 一元前缀,@ a
a.operator @(int) // 一元后缀,a @
- 注意隐式
this
- 一元前缀:没有参数
- 一元后缀:只有一个参数
- 二元运算符:参数是右边的数,用
this
指代左边的数
Friend
- 格式
friend type operator @ (arguments) {...}
- 用法
operator @(a, b) // 二元运算符,a @ b
operator @(a) // 一元前缀,@ a
operator @(a, int) // 一元后缀,a @
- 不用隐式
this
summary
表达式 | 成员函数 | 友元函数 |
---|---|---|
a + b | a.operator+(b) | operator+(a, b) |
a ++ | a.operator++(0) | operator++(a, 0) |
++a | a.operator++() | operator++(a) |
-a | a.operator-() | operator-(a) |
recommendation
运算符 | 推荐使用形式 |
---|---|
所有一元运算符 | 成员函数 |
=、()、[]、->、->* | 必须是成员函数 |
+=、-=、/=、*=、^=、&=、|=、>、>=、<、<= | 成员函数 |
其他所有二元运算符 | 非成员函数 |
Operators which can’t be overloaded
- 选择成员
.
- 通过指针选择成员
.*
- 作用域
::
- 三元条件表达式
?:
sizeof
typeid
Special operators
- 下标
[]
- 取值
->
- 自增自减
++
,--
- 赋值
=
- 转换
<<
,>>
13. Dynamic Object Creation
new
和new[]
返回一个指向该类型的指针- 一个由
new
创建的对象必须由delete
进行销毁 - 一个指针只能通过调用
delete
销毁一次
class A
{
int *p;
public:
A(int i) { cout << "Constructor" << endl; p=new int(i); }
~A() { cout << "Destructor" << endl; delete p; }
A(const A& r) // 拷贝构造函数
{
cout << "Copy-constructor" << endl;
p=new int(*r.p);
}
A& operator =(const A& r); // 赋值运算符重载
void output() { cout << p << "->" << *p << endl; }
};
A& A::operator =(const A& r)
{
cout << "Assignment" << endl;
if (this==&r) return *this; // 相等就不用赋值了
delete p; // 将原来的成员指针销毁
p=new int(*r.p); // 指针赋值
return *this;
}
14. Inheritance & Composition
The constructor initializer list
- 在创建派生类对象的时候,基类对象、成员和该派生类对象会依次被创建
- 基类和成员的创建顺序是按照其在派生类中声明的顺序,而不是构造函数的参数列表中初始化的顺序
#include <iostream>
using namespace std;
class X
{
int a;
public:
X (int i=0): a(i) { cout<<“Constructor X:”<<a<<endl; }
};
class Y: public X
{
int b;
X x1,x2;
public:
Y(int i, int j, int m, int n): b(i), x2(j), x1(m), X(n)
{
cout<<“Constructor Y:”<<b<<endl;
}
};
int main()
{
Y y(1,2,3,4);
return 0;
}
根据Y中的声明顺序,先创建基类X,再依次创建对象x1,x2,最后创建派生类对象y,而非其构造函数参数列表中的顺序。因此打印结果为
Constructor X: 4
Constructor X: 3
Constructor X: 2
Constructor Y: 1
Access control
继承方式\继承成员类型 | public | protected | private |
---|---|---|---|
public | public | protected | 不能访问 |
protected | protected | protected | 不能访问 |
private | private | private | 不能访问 |
Multiple inheritance
- 一个派生类有不止一个基类
class A {...};
class B {...};
class C : public A, protected B
{
int c;
...
}
##Ambiguity
- 两个不同的基类可能有同名的成员函数
- 两个不同的基类可能同时继承一个相同的类
解决方法:
- 使用作用域运算符
::
- 重新在派生类中定义一个同名的新函数,优先调用该函数
- 使用虚基类
class A
{
public:
void f() {};
};
class B : virtual public A {...};
class C : virtual public A {...};
class D : public B, public C {...};
void main( )
{
D d; // 创建顺序:A, B, C, D
d.f(); // ok
}
15. Polymorphism
Virtual functions
- 格式
virtual type function_name(arguments);
- 如果一个函数是基类中声明的虚函数,那么其在所有派生类中都是虚函数
- 在派生类中对虚函数进行重定义被称为覆盖(overriding)
class Instrument
{
public:
void play( ) const {cout<<"Instrument::play" << endl; }
};
class Wind : public Instrument
{
public:
void play( ) const { cout << "Wind::play" << endl; }
};
void tune( Instrument& i ) { i.play( ); }
int main()
{
Wind flute;
tune(flute); // Upcasting
}
由于tune()
参数为Instrument类型的引用,因此其调用的还是Instrument中的函数,而非Wind中的函数,打印结果为
Instrument::play
通过在基类中使用虚函数,并在派生类中对其进行覆盖可以解决这个问题。
class Instrument
{
public:
virtual void play( ) const // 虚函数
{
cout<<"Instrument::play" << endl;
}
};
class Wind : public Instrument {
public:
virtual void play( ) const {cout <<"Wind::play"< endl; }
};
void tune( Instrument& i ) { i.play( ); }
int main()
{
Wind flute;
tune(flute); // Upcasting
}
打印结果为
Wind::play
- 多态性的表现
- 派生类有
public
的基类 - 被调用的成员函数必须是
virtual
的 - 对象必须通过指针或引用进行操作(不然编译器会准确获得对象的类型,体现不了多态性)
- 派生类有
Overloading & Overriding
- 在派生类中,如果对基类中重载(overload)的函数进行覆盖(override),基类中其他重载的函数会被隐藏(hidden)
- 如果修改覆盖虚函数的返回类型,编译器将无法编译
Inheritance and the VTABLE
- 编译器为每个包含虚函数的类创建一个虚函数表(VTABLE)
- 编译器会给派生类创建一个新的虚函数表,并为其没有覆盖的虚函数使用基类函数地址插入新的函数地址
- 在每个具有虚函数的类中,它会秘密地放置一个VPTR指针,指向该对象的虚函数表
- 建立虚函数表、初始化VPTR、插入虚函数调用的代码——这些都会自动进行
// C15: NameHiding2.cpp
class Base
{
public:
virtual int f() const {cout<<"Base::f()\n";return 1;}
virtual void f(string) const {}
virtual void g() const {}
};
class Derived1 : public Base
{
public:
void g() const {}
};
class Derived2 : public Base
{
public:
int f() const {cout<<"Derived2::f()\n"; return 2;}
// Overriding
};
class Derived3 : public Base
{
public:
void f() const{ cout << "Derived3::f()\n";}
// error: 不能改变返回值类型
};
class Derived4 : public Base
{
public:
int f(int) const { cout << "Derived4::f()\n"; return 4;}
// notice: 参数列表被改变
};
int main()
{
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
d2.f(s); // error: 基类中f(string)被隐藏
Derived4 d4;
x = d4.f(1);
x = d4.f(); // error: 基类中f()被隐藏
d4.f(s); // error: 基类中f(string)被隐藏
// 通过引用和upcast体现多态性
Base& br = d4; // Upcast
x = br.f(1); // 派生类的f(int)被隐藏,基类中没有f(int)
x = br.f(); // 调用基类中的函数
br.f(s); // 调用基类中的函数
}
Abstract base classes & Pure virtual functions
- 纯虚函数带有关键字
virtual
且函数后有=0
- 一个抽象类至少含有一个虚函数
- 纯抽象类中只有纯虚函数
- 无法为抽象类创建一个对象
- 当抽象类被继承时,其派生类应对基类中的纯虚函数进行实现,否则该派生类也会成为抽象类
- 抽象类可以保证通过upcast使用指针和引用
Constructors & Destructors
- 构造函数不能是虚函数
- 析构函数可以是虚函数,且常为虚函数
- 只要基类的析构函数带有关键字
virtual
,那么其派生类中的析构函数哪怕没有关键字virtual
也是虚函数 - 为确保析构函数可以正常被调用,尽可能令析构函数是虚函数
16. Introduction to Templates
- 模版是源代码的复用,而继承和组合是对象代码的复用
Function Templates
template <Type argument list>
type function_name (formal arguments) {...}
- 类型参数(type argument):
typename T1
、typename T2
等,这些类型参数必须被实例化,当模版被使用时,它们会被具体的数据类型替代
#include <iostream>
using namespace std;
// 函数模版
template <typename T>
T getmax (T x,T y){ return (x>y) ? x : y ; }
int main()
{
int x1(1),y1(2);
double x2(3.4),y2(5.6);
char x3='a',y3='b';
cout<<getmax(x1,y1)<<endl; //T is instantiated by int
// 输出为2
cout<<getmax(x2,y2)<<endl; //T is instantiated by double
// 输出为5.6
cout<<getmax(x3,y3)<<endl; //T is instantiated by char
// 输出为b
return 0;
}
Class Templates
template <Type argument list>
class name {...};
- 如果成员函数定义在了类模版的外面,那么这些成员函数必须定义成函数模版的形式
#include <iostream>
using namespace std;
// 类模版
template <typename T1, typename T2>
class Test
{
public:
Test(T1 x,T2 y):a(x),b(y) { };
void Print();
private:
T1 a;
T2 b;
};
// member function
template <typename T1, typename T2>
void Test<T1,T2>::Print()
{
cout<<a<<", "<<b<<endl;
}
int main()
{
Test<int, double> obj1(10, 1.2);
Test<char, int> obj2('A', 2);
obj1.Print();
// 输出为10, 1.2
obj2.Print();
// 输出为A, 2
}