
面向对象程序设计
引言
本笔记基于cx老师的上课记录与xuan的笔记 感谢老师与前辈!
导航
- 引言
- 导航
- week 1
- week 2
- week 3–STL
- week 4–memory and reference
- week 5–class
- week6–composition and inheritance
- week7 polymorphism
- week8 copy constructor
- week9–overloaded operators
- week10–template
- week11–iterators and I/O stream
- week12–exceptions
- week 13–smart pointer
- week 14–miscellaneous topics
- homework explanation
- history final exam
week 1
C++的介绍
the first c++ programme
1 |
|
- 注意:一般规范书写还是要返回
read input
1 |
|
Using Objects
The string class
1 |
|
string class 的应用
1 |
|
File I/O
1 |
|
I/O example
1 |
|
regex example(替换字符)
1 |
|
week 2
A Quick Tour of C++
- 这一节课cx老师通过介绍排序算法引出c++中的模板、自定义类、类的继承,快速介绍了c++中的几个性质(cx老师也说如果这一节课的每个步骤完全弄懂,这门课不用来听了
bushi) - 首先用c++写了选择排序的程序,和C语言大差不差
- 以下的例子介绍了C++中的在函数定义中显式声明模板参数。这样我们对于不同的变量类型都可以适用。
1 |
|
设计结构体来排序
- 引用传递(&):直接访问原始对象,这样能方便我们不额外开辟一个空间,直接修改原始对象。(当然对于常量变量并不会修改原始对象)
- 值传递:会额外开辟一个原始对象的副本。
- 对于我们的选择排序对于我们自定义的变量,我们需要自己定义我们的比较规则与输出规则。
1
2
3
4
5
6
7
8
9
10
11struct student{
int id;
std::string name;
};
bool operator<(const student &s1,const student &s2){
return s1.id<s2.id;
}
std::ostream &operator<<(std::ostream&out,const student& s)
{
return out << "("<<s.id<<","<<s.name<<")";
}引入自定义类,class(其中类里面的字段为private),而前面的结构体其实本质上的字段是public,所以我们在main函数中对class进行初始化程序会报错。这就涉及到我们在自定义类构造函数。
1 |
|
- 我们思考是否有一个抽象类,类似于前面的抽象类型来管理多个性质与成员相同的类。这就涉及c++的继承
- 注意抽象类的定义,抽象类定义为纯虚函数,即没有函数体,只有函数声明,其子类必须实现该函数。子类实现的函数的值存在在抽象类的protected域中。
- 继承的语法为:class 子类:访问修饰符 父类{};
- 子类实现父类的纯虚函数时,子类必须使用override关键字来修饰。
- 此时在main函数中我们通常涉及指针数组来方便操作对象
1 |
|
- 我们又仿照前面的结构体,按照下面的代码想要输出我们刚刚得到的结果,但是发现无法访问area和perimeter。因为我们的shape类的成员变量area和perimeter是protected的,所以无法访问。
- 我们可以在Shape类中定义一个输出函数并采取friend字段。
- 我们却发现输出的流却像地址,我们回到我们的打印的模板,此时的T其实是Shape*,所以此时的arr的类型就是指针,所以输出的流便是地址。
- 我们可以额外再写一个T抽象函数专门针对指针类型
- 我们继续前进:修改shape类,添加一个虚拟函数,想要输出每个类的名字,同时每个子类都实现这个函数。但是我们会发现程序报错:
- 因为我们输出函数中的Shape变量前有const修饰,说明我们不能改变它的状态,所以我们在输出函数中调用name()函数是不可行的。
- 我们想要解决需要在shape类中对name()函数用const修饰。显式告诉编译器我们在函数中不会改变状态。
1 |
|
- 我们再回到这堂课的最初出发点:selection sort,我们现在想要根据面积和周长分别进行排序,那这样我们需要添加一个selection_sort函数和find_min函数,因为此时我们的接口多了一个自定义的比较函数。
- 在这里需要注意的是我们的自定义的比较函数不能直接访问Shape类的成员变量,我们需要在Shape类中定义get函数。
- 其实当我们定义好get函数后,我们可以将Shape类中的freind修饰的输出函数删除,我们的输出函数可以通过调用get函数而不需要访问Shape类的成员变量。
- 除了单独书写一个比较函数,我们还可以在主函数调用时直接在主函数中传入比较函数。
1 |
|
week 3–STL
STL
What is STL
- C++的标准模板库的一部分
- 封装C++的数据结构与算法
- 包含:
- 容器:class templates,common data structures
- 算法
- 迭代器:泛化的指针,在容器与算法间打交道
Why should I use STL
- 节省时间与工作量
- 增加程序可读性
Containers
- 线性容器
- array(static),vector(dynamic)
- deque(double-ended queue)
- forward_list(signlely linked list),list(doubly linked list)
- 关联性容器(本质上是用红黑树)
- set(collection of unique keys)
- map(collection of key-value pairs)
- multiset,multimap
- Unordered associative
- hashed by keys
- unordered_set,unordered_map
- unordered_multiset,unordered_multimap
- Adaptors
- stack
- queue
- priority_queue
vector example
1 |
|
other containers
1 |
|
Algorithms
- works on a range defined as [first,last]
- for_each,find,count
- copy,fill,transform,replace,rotate
- sort,partial_sort,nth_element
- set_difference,set_union
- min_element,max_element
- accumulate,partial_sum
ex
1 |
|
iterators
- connect containers and algorithms
- 后面的课会讲到
pitfalls
access safety
- accessing an element out of range
- use push_back() for dynamic expansion
- preallocate with constructor
- reallocate with resize()
silent insertion
- map<>中如果没有对应的pair,可能悄悄添加
- 通常用count() or contains()(基于c++20)来检查
size() on list<>
- my_list.size() might cost linear time before C++11
- Constant time guaranteed:my_list.empty()
invalid iterator
- using invalid iterator
1
2
3
4
5
6
7list <int> L;
list <int>:: iterator li;
li=L.begin();
L.erase(li);
++li;//wrong
//我们需要重新调整
li=L.erase(li);
week 4–memory and reference
memory model
what are these variables?
1 |
|
分配位置
不同的变量介绍
- 全局变量(global)
- vars defined outside any functions
- can be shared btw .cpp files
- extern(用其他模块的全局变量,编译时要和定义这个变量的模块一起编译)
3.1 extern is a declaration says there will be such a variable somewhere in the whole program
3.2 “such a” means the type and the name of the variable
3.3 global variable is a definition , the place for that variable - static
4.1 static global variable inhibits access from outside the .cpp file(只有在本模块使用)
4.2 so as the static function
4.3 static local variable keeps value between visits to the same function(存储与全局变量相同,并且第一次调用时初始化)
指针
1 |
|
- operators with pointers
- get address
- get the object
- call the function
- two ways to access
- string s;
1.1 s is the object itself
1.2 at this line,object s is created and initialized - string *ps;
2.1 ps is a pointer to an object
2.2 the object ps points to is not konwn yet
- string s;
Reference
Defining references
- references are a new data type in C++
- type& refname =name;
- for ordinary variable definitions
- an initial value is required
- type& refname;
- In parameter lists or member variables
- Binding defined by caller or constructor
1 |
|
Rules of references
- 引用变量创造时必须初始化
- 初始化建立了binding,并且不能再重新和另一个变量绑定
1 |
|
- 引用变量的本质就是给已经存在的变量多了个名字
- non-const的reference不能绑定rvalue,引用的non-const的目标是lvalue(能放在等号左侧的表达式)。
1 |
|
- 悬空引用(dangling reference)出现报错,引用必须绑定到生命周期足够长的对象上
1 |
|
Type restrictions
- No references to references
- No pointers to references,but reference to pointer is ok(指针变量是一个健全的类型可以独立存在)
1 |
|
- No arrays of references
引用与重载解析
- 将一个 int 类型的变量传递给 int 类型的参数和 int & 类型的参数的优先级是一样的。如果存在相关的两个函数则会出现
ambigous overload
的报错 - 将 int 类型的变量传递给 int 类型的参数和 const int & 类型的参数的优先级也是一样的
- 不过,如果有两个重载,它们在某一个参数上的唯一区别是 int & 和 const int &,而 int 类型的变量绑定给这两种参数都是可行的,此时 int & 的更优
Dynamically allocated memory
Dynamic memory allocation
- new expression
new int;
new Stash;
new int[10]; - delete expression
delete p;
delete [] p; - new与malloc的差异在于:new在动态分配内存的同时还通过构造函数初始化对象,我们下面的例子就说明了这点。
- 同时对于数组的删除,我们可以发现删除的顺序是从后往前删除的。
- 注意对于数组的删除采取 delete [] p;但是如果我们写delete p,只能删除第一个元素。
- new、delete和malloc、free不能混用。
1 |
|
- 下面的例子告诉我们new出来的东西一定要及时删干净,否则迟早占用完内存。并且内存不能释放两次。
- 还需要区分被释放的空间和零指针NULL没有任何关系。也就是说NULL也占据了动态空间的。
1 |
|
const
- 初始化以后不能再更改值,但是可以使用
- run-time constants(若在程序跑起来的时候才能确定常量的值,则可能报错)
- 有关const和指针
const int *
和int const *
表示指向一个不可变的int的指针,并且const int *
不能转化为int *
int * const
表示指向一个int的不可变的指针const int * const
和int const * const
表示指向一个不可变的int的不可变的指针
- 将一个临时对象绑定给一个 const 引用,这个临时对象的生命周期被延长以匹配这个 const 引用的生命周期。
const Matrix & m = m1 - m2;
,但是临时对象不允许绑定给non-const引用
1 |
|
- pointers with const
1 |
|
String literals
- 对于初始化的字符串,其实本质上就是初始化的常量字符串,所以不能修改里面的字符。(如果从内存分布的底层逻辑来说,初始化字符串本质上是一个指针指向代码段上方的一个字符串,(并且同样的字符串在代码段只会存一份),所以我们并不能通过更改指针来得到想要的结果)
- 如果想要修改字符,初始化时应该使用字符数组
- 当我们使用指针初始化字符串时只能使用const进行修饰,否则会报错。
passing addresses
- 当对象较大时可以传递地址(采用指针或者引用)
- 传参时通常加const修饰,表示该参数不会被修改
1 |
|
week 5–class
5.1 class
5.1.1 类的定义
- 在C++中用类来定义变量时可以不像C语言那样使用结构体来定义变量,当我们已经定义好类后,可以使用形如
Foo x
来定义变量。 - 当然当出现变量名相同时,为了避免冲突C++也可以使用显式地结构体的方式定义变量,
struct Foo x; int x;
- Forward declaration:如果当前的作用域还没有定义好类,但是我们提前声明了类
Struct X;
,这就是forward declaration。不完整的类型不能用来定义变量(包括成员变量)、作为函数声明或定义的参数或者返回值类型等;但是可以定义指向它的指针。
5.1.2 类的成员
- 类型别名的声明–using
- C++11引入了using来声明类型别名
- 类型别名声明也可以是类的成员,它的地位与静态变量类似,我们访问时通过类名作用域解析运算符::,(但是静态变量也可以通过实例访问)
- this指针
- 在成员函数的函数体中,访问任何成员时都会被自动添加
this->
- 在成员函数的函数体中,访问任何成员时都会被自动添加
- 成员函数不能重新声明
5.1.3 构造函数
构造函数是类对象初始化的一部分,当对象被创建时,构造函数被调用。
需要注意的是构造函数时可以有参数的,这种情况下创建对象时需要附加参数(
Container c2(64)
),而在无参构造的情况下不能加括号(Container c3
),如果加括号可能与函数声明存在歧义。构造函数必须是public,否则对象无法被构造
如果代码并没有显式创建构造函数,那么编译器会生成一个默认的构造函数(default constructor),当然如果用户提供了构造函数,用户可以使用
ClassName()=default;
来引入默认构造函数,
同样,用户可以通过ClassName()=delete;
显式将默认构造函数deletedmember initializer list
- 为了方便构造函数的初始化,C++11引入了member initializer list
- 如下面的代码的情况:
Point c;
是告诉编译器Circle类有一个成员变量c,在创建Circle对象时,会真正分配内存并初始化c。 - 在存在类成员时,member initializer list是有必要的。如下面的例子:需要注意的是因为我们的Point类中并没有定义default constructor,如果我们不使用member initializer list,在
Point c;
时就会调用默认构造函数,这会报错。(本质上不使用member initializer list,我们的构造函数内部只是赋值例如c=Point(cx,cy)
,所以会出现上述情况) - 在下面的例子中,如果我们已经存在了member initializer lists,那么我们前面的
Point c;
就变成了类的成员变量声明,此时我们就不能通过Point c(0,0);//Error!
直接调用构造函数。但是C++提供了另外一种思路,我们还可以通过默认成员初始化器Point c{0,0}; Point c=Point(0,0);
来构造。如果一个成员变量同时被 member initializer list 指定且有 default member initializer,按前者执行,后者被忽略。 - 如果构造函数的定义与声明分离,则member initializer lists应当出现在定义中。
- member initializer list 的顺序不影响成员被初始化的顺序,它们按照在类定义中的顺序初始化。
1
2
3
4
5
6
7
8
9
10
11
12class Point {
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
};
class Circle {
Point c;
int r;
public:
Circle(int cx, int cy, int r) : c(cx, cy), r(r) {}
};
5.1.4 析构函数
- 析构函数在每个对象的生命周期结束的时候被调用,大多数情况被用来释放对象在运行过程中可能获取的资源。
5.1.5 构造和析构的时机和顺序
- 在下面的情况下,构造函数会被调用:
- 对于全局对象,在 main() 函数运行之前,或者在同一个编译单元内定义的任一函数或对象被使用之前。在同一个编译单元内,它们的构造函数按照声明的顺序初始化。
- 对于 static local variables(静态变量),在第一次运行到它的声明的时候。
- 对于 automatic storage duration 的对象(局部变量),在其声明被运行时。
- 对于 dynamic storage duration 的对象,在其用
new
表达式创建时。
- 在下面的情况下,析构函数会被调用:
- 对于 static storage duration 的对象,在程序结束时,按照与构造相反的顺序。(因为静态变量和全局变量都存储在数据段中,所以它们的析构函数调用都是在程序结束后调用)
- 对于 automatic storage duration 的对象,在所在的 block 退出时,按照与构造相反的顺序。
- 对于 dynamic storage duration 的对象,在
delete
表达式中。 - 对于临时对象,当其生命周期结束时。
- 数组元素的析构调用顺序与其构造顺序相反,类的成员的析构函数的调用顺序也是如此。
- example
1 |
|
输出结果:111 2 3 4 5 6 444 333 888 999 777 666 555
5.2 objects
- objects = attributes+services
5.2.1 成员变量的类型
public
:成员变量和成员函数对外部可见private
:被修饰的成员变量和成员函数不能在类外被访问,只能在类的成员函数内访问或调用。(防止外部代码窃取数据,篡改数据)
5.2.2 object vs class
- objects
- Represent things,events
- Respond to messages at run-time
- Classes
- Define properties of instances
- Act like native-types in C++
5.2.3 静态变量
1 |
|
在类定义中,用static声明没有绑定到类的实例中的成员。也就是上面 User 类的每个实例里仍然只有 id 而没有 tot。不过,语法仍然允许用一个类的实例访问 static 成员,例如 user.tot。静态成员也受 access specifier 的影响。
default member initializer 和 member initializer list 是针对 non-static 成员变量的,它们对于 static 成员变量不适用:也就是说,在类中的 static 成员变量只是声明。也就是说,我们必须在类外给出其定义,才能让编译器知道在哪里构造这些成员
static 成员函数:我们可以使用 User::getTot() 来调用这个函数,当然也允许通过一个具体的对象调用这个函数;但是调用时不会有 this 指针。这个性质也说明static 成员函数只能访问静态成员变量。
局部静态变量
- 初始化仅发生一次:无论函数被调用多少次,初始化(如 count = 0)仅在第一次执行时进行。
- 只能在定义它的函数内访问,但生命周期持续到程序结束。无法通过外部访问
静态变量与普通成员变量
静态变量与普通成员变量的区别
特性 静态变量 (static) 普通成员变量
生命周期 整个程序运行期间 实例的生命周期
存储位置 全局数据区 实例的内存空间
共享性 所有实例共享 每个实例独立
访问方式 类名或实例访问 只能通过实例访问
初始化 必须在类外初始化 可以在构造函数中初始化
5.2.4 const成员
- 声明为 const 的成员函数称为 const 成员函数,它**保证不会修改 *this 的值**;即调用这个成员函数的对象不会被更改。
- 在 const 成员函数中,试图调用其他非 const 成员函数,或者更改成员变量都是不合法的(但是const成员函数允许修改被
mutable
修饰的成员变量) - 同时,const对象也只能调用const成员函数
5.3 C++中class与struct的差别
- 在C++中class和struct的唯一区别是:class的所有成员默认是private的,而struct的成员默认是public的。其他没有任何差异。
5.4 编译单元
- 一个cpp文件就是一个编译单元
- header=interface,在里面存放函数的签名以及全局变量的定义,而具体的实现都在cpp文件中完成。
- 并且同一个变量的定义只能出现在同一个编译单元。
5.5 inline函数
- inline函数就是将函数的实现直接嵌入到调用它的地方,从而达到减少函数调用开销的目的。
- 而在class中,如果函数主体放置在class中,那么这个函数就是inline函数。而inline函数如果放置在一个.h头文件中,并不会引起重定义的问题。
- 适用场景
短小函数(如简单的 getter/setter)。
高频调用函数(如循环内的操作)。
替代宏(类型安全,避免宏的副作用)
week6–composition and inheritance
composition
组合的概念
它通过将一个类的对象作为另一个类的成员变量来实现。相比于继承,组合强调“拥有关系”而非“是一种”。
这种对象我们也叫做嵌入式对象
- 嵌入式对象都会被初始化,如果没有显式初始化,则调用默认构造函数。但如果没有默认构造函数,就必须在现在的类的初始化列表中提供参数进行显式初始化。
- 如果存在多个嵌入式对象,嵌入式对象的构造顺序由初始化顺序决定,而不会受到初始化列表顺序影响。
1
2
3
4
5
6
7
8
9
10
11class A {
public:
A(int x) { } // 没有默认构造函数
};
class B {
private:
A a;
public:
B() : a(42) { } // 必须显式初始化
};
inheritance
继承的概念
- 它是类与类之间的关系,一个类可以继承另一个类。继承关系是“是”的关系。
继承的优势
- 避免代码重复
- 重复利用代码
- 可维护性
- 可扩展性
more on constructors
- 基类首先被构造
- If no explicit arguments are passed to base class,the default constructor is called.
- 析构函数的调用顺序相反
继承的示例以及访问控制
example
1 |
|
成员变量的访问权限
继承类型
- 当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
派生类能够继承什么呢?
- 一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数
week7 polymorphism
多态的实现
- 继承多态性是指当基类的指针(或引用)绑定到派生类对象上,通过此指针调用基类的成员函数时,实际上调用到的是该函数在派生类中的覆盖函数版本。
虚函数
- 用virtual关键字修饰的成员函数,Virtual关键字其实质是告知编译系统,被指定为virtual 的函数采用动态联编的形式编译 只有类的成员函数才能声明为虚函数,普通函数(不属于任何类)不能定义为虚函数。拥有虚函数的类也称为多态类。
基类与派生类的赋值相容性
- 派生类对象可以赋值给基类对象
- 派生类对象的地址可以赋值给指向基类对象的指针。
- 派生类对象可以作为基类对象的引用。
- 存在的问题:通过这些赋值方式,只能访问派生类对象从基类继承的成员,无法访问派生类自己定义的成员。(向上转型)
虚函数的特性
- 一旦将某个成员函数声明为虚函数后,它在继承体系中就永远为虚函数了
- 如果基类定义了虚函数,当通过基类指针或引用调用派生类对象时,将访问到它们实际所指对象中的虚函数版本。
- 派生类通过从基类继承的成员函数调用虚函数时,将访问到派生类中的版本。
- 只有类的非静态成员函数才能被定义为虚函数,类的构造函数和静态成员函数以及内联函数不能定义为虚函数
虚函数的意义
- 虚函数:它允许通过基类指针或引用调用派生类重写的成员函数。也就是说,当派生类重写了基类的虚函数时:如果通过基类指针或引用调用该函数,实际执行的是派生类中的版本(而不是基类中的版本)
override
- 显式地标记派生类中重写基类虚函数的成员函数。override 的主要作用是让编译器帮助检查:
- 该函数确实是在重写基类的虚函数
- 基类中存在相同签名的虚函数
- 函数签名完全匹配(包括参数类型、const限定符等)
如果不符合重写条件,编译器会报错,帮助及早发现错误。
final
- 可以作用于类,当类被声明为final时,该类不能被继承。
- final 用于限定只想让派生类继承,而不允许被覆盖的虚成员
函数。final 只能限定虚函数,被限定为final 的成员函数,则任何派生类对该函数的覆盖定义都是错误的。
协变返回
- C++ 的虚函数重写规则
函数签名必须严格匹配(除了协变返回类型)。
协变返回类型仅适用于指针/引用:
Expr* → BinaryExpr* ✅
Expr& → BinaryExpr& ✅
Expr → BinaryExpr ❌(值类型不协变)
虚函数的限制
- 普通函数(非成员函数)
原因:虚函数必须是类的成员函数,普通函数(全局函数或命名空间内的函数)没有 this 指针,无法通过虚函数表(vtable)动态调用。 - 静态函数
- 构造函数:构造函数用于初始化对象,此时对象尚未完全创建,虚函数表(vtable)可能未初始化。
- 友元函数不属于类的成员函数,即使声明在类内,也无法成为虚函数。
- 模板函数需要在编译时实例化,而虚函数表需要在运行时动态绑定,两者机制冲突
虚析构函数
- 假定使用delete来销毁一个指向派生类的基类指针,如果
基类析构函数不是虚函数,就如一个普通成员函数那样,delete函
数调用的就是基类析构函数,而不会调用派生类的析构函数。这样,在通过基类对象的引用或指针调用派生类对象时,将致使对
象析构不彻底。
纯虚函数与抽象类
纯虚函数
- 纯虚函数是一种特殊的虚函数,它在基类中没有实现(定义),只有声明,要求派生类必须重写实现。
1 |
|
抽象类
- 包含至少一个纯虚函数的类
- 不能实例化:不能创建抽象类的对象
Shape s; // 错误! 不能实例化抽象类
- 用作接口:定义派生类必须实现的接口规范
- 可以包含普通成员:可以有数据成员和普通成员函数
- 可以有构造函数:虽然不能实例化,但派生类构造函数会调用它
1 |
|
week8 copy constructor
8.1 拷贝构造函数
- 处理形如
Matrix m =m1;
的初始化,C++引入了拷贝构造函数,它是一种特殊的构造函数。 - 具体格式:
- 对于Class T,拷贝构造函数的第一个参数是
const T&
或者T&
。 - 拷贝构造函数在没有用户定义版本的时候会声明一个默认的拷贝构造函数。
- 对于Class T,拷贝构造函数的第一个参数是
- 拷贝构造函数被调用的场景:
- 初始化,
T t = t1; T t(t1); T t = T(t1);
- 函数参数传递,例如
f(t);
,其中函数签名是void f(T t)
- 函数返回,返回一个对象,例如
T f();
,在返回处构造再拷贝给调用处。
- 初始化,
copy elision
- 针对上面的函数返回的情形,函数在返回时构造一个 T 类型的临时对象,把它作为返回值;此时这个临时对象作为返回值会被用来初始化调用处的那个临时对象(拷贝构造),然后被析构。随后该语句结束,临时对象被析构。也就是说,有两次构造(其中一次是拷贝构造)和两次析构发生。
- 但是显然这种情况是可以优化的,我们如果能直接在调用处构造,就可以省略在函数返回处的临时对象的构造和析构。
- 从C++17开始,以下两种情况对拷贝的省略是强制的
返回纯右值时的直接构造
- 函数返回一个纯右值,且返回值类型与函数声明类型一致。
- 例子
1
2
3
4T create() {
return T(); // 纯右值:直接构造到调用处的目标地址(C++17 强制优化)
}
T obj = create(); // 无临时对象,直接构造 obj初始化对象时的临时物化省略
- 用纯右值直接初始化另一个同类型对象
- 例子
1
2
3T obj = T(); // 直接构造 obj,无临时对象(C++17 强制优化)
void foo(T t);
foo(T()); // 直接构造参数 t,无临时对象
8.2 Special Member Functions
- 我们前面提及的默认的构造函数,拷贝构造函数等统称为特殊成员函数。它们的共同特点是,如果没有用户显式声明的版本,编译器会生成默认的声明;如果需要使用,则编译器生成默认的定义。
Rule of Three
- 如果用户需要自定义一个类的拷贝构造、拷贝赋值或者析构函数,那么基本上这三个都是必要的,也就是说用户都需要自己定义。
8.3 拷贝构造函数与赋值函数的例子
- 总结,析构函数,拷贝构造函数,赋值操作符这三个函数如果系统生成有误,那么我们自己需要书写
- 通常涉及指针操作时,此时系统自动生成的函数会存在问题,就如同下面的例子
1 |
|
8.4 在容器中的拷贝函数
- 因为我们的vector在不断扩容(在vector库中定义的是每次扩容两倍),所以会存在额外的copy操作,如果我们指定vector大小就可以避免额外的copy操作产生
- 如果我们使用emplace_back,直接在容器内部构造对象就可以避免额外copy
1 |
|
week9–overloaded operators
- 重载运算符,让用户定义的运算符具有运算能力
- 调用函数的另一种方式
9.1 拷贝赋值运算符
- 引入:对于代码
c1 = c2
;对于两个类的实例,如果实现这个操作以后,那么两个容器现在指向同一块内存,这样的操作其实有点类似于引用,并没有真正构造一个副本。 - 为了解决这个问题,C++允许用户重载赋值运算符。(注意一般需要防止
x=x
导致的内存泄露) - 在一个有运算符的表达式中,如果至少有一个操作符是某个类的对象,则由重载解析查找相应的函数。
- 如果用户没有显式地给出 operator=,那么编译器会生成一个 public的默认拷贝赋值运算符的声明;如果它被使用,则编译器生成它的定义;它完成的内容即为将各个成员变量用它们 operator= 拷贝一遍
9.2 Restrictions
- 不能重载不存在的运算符
- 优先级和结合律是不变的,并且原运算符的参数数量不能发生改变
- Operators must be overloaded on a class or enumeration type
- 这些运算符不能被重载::: (scope resolution), . (member access), .* (member access through pointer to member), and ?: (ternary conditional)
- 对 = (assignment), () (function call), [] (subscript), -> (member access) 的重载必须是成员,而不能是全局函数
9.3 C++ overloaded operator
- 作为一个成员函数
String String::operator+(const String& that);
- 运算符重载时第一个argument是隐式的,这也就不能在接收器执行类型转化,也就是说,
z=3+y //ERROR z=y+3 //Good
- 并且我们一般希望返回的对象是不可修改的,所以通常使用const进行修饰。
- 需要注意的是private成员的访问权限是基于类的,所以成员函数能够灵活操作其他对象的私有数据。
- assignment operator必须是成员函数,一元运算符也最好是成员函数。
- 作为全局函数
String operator+(const String& lhs,const String& rhs)
- 运算符重载的argument都是显式的
- 因为全局函数不能访问类的私有变量,所以在有些情况下需要在类中使用friend声明友元。(但是友元只是一种权限的声明,友元函数并非类的成员)
9.3.1 Argument Passing
- 算术运算
- const T operator X(const T&l,const T&r)
- 返回的必须是一个全新的对象,这是为了符合c++语义,防止(a+b)=c等代码合法。
- 逻辑运算
- bool operator X(const T&l,const T&r)
- []
- E& T::operator[] (int index)
- 在C++中,operator[] 是下标访问运算符,用于重载 [] 操作符,使对象可以像数组一样通过索引访问元素。它必须是一个类的成员函数,不能是全局函数。
- 通常返回引用,允许通过[]修改对象内部数据
- 但是它的重载也可以有另一种写法,在作业中也提到过,我们不需要修改值,只是返回对象,在这种情况下需要用const进行修饰。
- 前缀与后缀++、–
- 前缀
- const Integer& operator++(); //前缀++
- this +=1;returnthis;
- 后缀
- const Integer operator++(int); //后缀++
- Integer old(* this); //fetch
- ++(*this);//increment,调用前缀实现的函数
- return old;
- 其实通常情况下当这些重载运算符存在逻辑自洽的关系时,即这个重载运算符可以通过我们已经定义好的重载运算符实现时,编译器通常可以帮我们实现。
- 前缀
- stream extractor
- ostream &operator <<(ostream &,const A &a)
- ostream 不能是const,因为流的工作原理:当我们cout时,我们需要对其内部进行修改。
例子
1 |
|
9.4 Type conversion
- 想将 T 转化为 C, 那么需要一个 C(T) 的不加 explicit的构造函数,或者 operator C() 的重载。如果两个都有,编译器会出错。
week10–template
- 模板编程属于泛型编程,把变量的类型当作参数来声明。
- 多文件,模板一定要放到
.h
文件中,模板只是声明。
function templates
1 |
|
template
下面的内容就是模板,这里就是函数模板T
是模板参数,T
是类型,T&
是引用类型,T*
是指针类型。class means any built-in type or user-defined type.
template instantiation
1 |
|
- 这里swap会调用函数模板,随后生成float swap,编译器会插入函数并调用。并且C++编译后的重载函数的名字会与原来有所不同,会把函数的参数类型编入函数的名字。
- 模板函数不能同时实例化多个类型.
swap(int,double) //error
,也就是说在实例化过程中不会进行参数类型转换 - 并且普通函数的优先级高于模板函数,编译器首先寻找普通函数,如果普通函数存在,则调用普通函数,否则调用模板函数。
- 需要注意的是,在构造指向某个类型的指针时
Container<char>* p
,隐式实例化并不会发生,因为不需要这个类型的完整信息。
1 |
|
- 上述的第一个调用函数错误的原因主要在于模板参数推导发生在重载解析与隐式转化之前,编译器尝试推导 T 时发现 a 和 b 类型不一致,无法确定 T 应该是 int 还是 double,因此报错。
- 第二个调用函数显式指定模板参数就能够解决。
默认模板参数
- 默认参数必须从最右边的参数开始定义,不能跳过中间参数
class templates
1 |
|
- 模板类中可以有多个模板参数,包括类型参数和非类型参数( 通常可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针– 但不允许用浮点型(或双精度型)、类对象或void作为非类型参数。)
- 类模板里的函数都是函数模板
- 成员函数的定义需要模板(注意,类模板的函数是声明不是定义),就如同下面的例子,一定要加上
vector<T>::
1 |
|
- templates 能够使用多种类型
1 |
|
- template嵌套:
vector<vector<double *>>
模板参数类型推导
- 传值模板参数
- 编译器在推导模板参数param类型的过程中,会去除所有加在实参上的类型限定符(包括const 、&和&&)以体现函数的值形参语义。因为这样才能够保证模板函数值形参可复制的语义(函数调用时将实参的值复制到值形参变量中)
- 左值引用模板参数
- 当类型参数Param为可变引用(左值引用)时,可以向它传递能够推断出地址的实参,如果是const类型的实参,将保留它的const限定。但是,不能够接受字面常量和临时变量,这是因为引用参数是可以修改实参值的,但将常量和临时变量传给引用参数,就与该语义相违背了。
- 当类型参数为左值引用时,无论实参是左值还是右值,推断出的模板参数都是左值。
- 右值引用模板参数
- 形如T&&这样的右值引用参数,既可以接受左值引用实参,也可以接受右值引用实参,通常被称为转发引用。
模板相关的继承
模板类可以继承非模板类,也可以继承模板类(需要实例化)
- 模板类直接继承一个普通的非模板类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 非模板基类
class Base {
public:
void display() {
std::cout << "Base class" << std::endl;
}
};
// 模板类继承非模板类
template <typename T>
class Derived : public Base {
public:
void show(T value) {
std::cout << "Derived class with value: " << value << std::endl;
}
};
// 使用示例
int main() {
Derived<int> d;
d.display(); // 继承自Base
d.show(42); // Derived的成员函数
return 0;
}- 模板类可以继承另一个模板类的实例化版本,这意味着基类模板必须用具体的模板参数实例化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 模板基类
template <typename U>
class TemplateBase {
public:
void display(U value) {
std::cout << "TemplateBase with value: " << value << std::endl;
}
};
// 模板类继承模板类的int实例化版本
template <typename T>
class DerivedFromTemplate : public TemplateBase<int> {
public:
void show(T value) {
std::cout << "DerivedFromTemplate with value: " << value << std::endl;
}
};
// 使用示例
int main() {
DerivedFromTemplate<double> d;
d.display(10); // 继承自TemplateBase<int>
d.show(3.14); // DerivedFromTemplate的成员函数
return 0;
}- 派生类模板继承自一个未被实例化的模板基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 模板基类
template <typename U>
class TemplateBase {
public:
void display(U value) {
std::cout << "TemplateBase with value: " << value << std::endl;
}
};
// 模板类继承模板基类(使用不同的模板参数)
template <typename T>
class DerivedTemplate : public TemplateBase<T> {
public:
void show(T value) {
std::cout << "DerivedTemplate with value: " << value << std::endl;
}
};
// 使用示例
int main() {
DerivedTemplate<std::string> d;
d.display("Hello"); // 继承自TemplateBase<std::string>
d.show("World"); // DerivedTemplate的成员函数
return 0;
}
expression parameter
- 模板中可以声明一些常数,这些常数在模板中声明,在模板函数中可以访问。
模板重载、特化、非模板函数及调用次序
模板特化
- 模板特化的原因
- 模板虽然能够实例化出实用于各种数据类型的可用函数或类,但要让一个模板实现对全部数据类型的正确处理,却不一定做得到。比如,在例7-9中,max模板就不能够正确计算字符串的最大
值。因为,char*类型的字符串需要用strcmp函数而不是“<”运算符比较其大小。 - 为了解决这一问题,C++允许为模板定义针对某种数据类型的替代版本,称为模板的特化。
- 模板特化的语法形式
template <>
用具体类型替换模板参数的函数模板或类模板
<>中不需要任何内容,表示模板特化。
函数参数匹配与调用次序
- 优先调用普通函数
- 如果没有普通函数,优先调用模板特化函数;
- 当没有普通函数和特化模板函数时才调用模板函数。如果有多个重载模板函数都符合要求,就选择精确匹配的模板函数;
week11–iterators and I/O stream
链式输入和输出
cin和cout是
<iostream>
提供的两个全局对象,它们所处在命名空间std中。std::cin 的类型是
std::istream
(input stream),它其中对各种基本类型重载了operator>>
.我们考虑 cin >> x >> y; 的运行顺序。首先 cin >> x 被运行,因此这个表达式就类似于 (cin.operator>>(x)) >> y;,而 cin.operator>>(x) 运行结束后返回 cin 本身,剩下的表达式就是 cin >> y; 了。因此,返回 *this 的好处就是能够实现这种链式的读入。1
2
3
4
5
6
7
8istream & istream::operator>>(int & value) {
// ... extract (read) an int from the stream
return *this;
}
istream & istream::operator>>(double & value) {
// ... extract (read) a double from the stream
return *this;
}同样地,我们也能够为自己的类完成对
>>
和<<
的重载。只是需要注意的是一般只能作为全局函数重载,因此当需要访问类的私有变量时,注意声明为友元。
1 |
|
konwledge of iterators
- 我们通过指针能够完成「找到下一个元素」「访问元素内容」以及「比较」的功能,也就是迭代时所需的功能。因此我们将指针称为数组的 迭代器 (iterator)。迭代器并不是一个具体的数据类型,而是一类东西的统称。
- 对于一般的数据结构,可能并没有这样的指针,在这种情况下需要自定义迭代器,达到能够和数组的指针一样遍历容器。
- 通常迭代器的使用基于range-based for loop.使用这种 range-based for loop,只需要类“包含成员函数 begin() 和 end() ,且其返回值类型支持运算符 ++ 、 * 和 != ”
the classification of iterators
- Input Iterator : 能够用来标识、 遍历一个容器中的元素,能够从所指的元素中读取值
- Input Iterator只需要保证单趟算法的有效性:一旦一个 Input Iterator it 被 ++ 后,它之前所指的值及其所有拷贝都不再需要保证有效性
- Forward Iterator : 在 Input Iterator 的基础上,能够支持多趟算法
- Bidirectional Iterator : 在 Forward Iterator 的基础上,能够双向移动
- RandomAccess Iterator : 在 Bidirectional Iterator 的基础上,能够在常数时间内移动从而指向任一元素
- Contiguous Iterator (C++17) : 在 RandomAccess Iterator 的基础上,逻辑上相邻的元素在内存里物理上也相邻。
- 指向数组中元素的指针满足 Contiguous Iterator 的所有要求
week12–exceptions
example
1 |
|
When to use exceptions
- many times,yo don’t know what to do
- anything can go wrong
how to raise an exception
1 |
|
What about your caller?
1 |
|
- Mildly interested
1 |
|
exception handlers
- select exception by type
- can re-raise
- 用noexcept修饰某个函数,则这个函数不会抛出异常
- two forms
catch (SomeType v){//handler code}
catch (...){//handler code}
- 注意在catch块捕获时不会进行数据类型的默认转换,强调精准匹配。如果异常不能被任何catch块捕获,它将被传递给系统的异常处理模块,程序将被系统异常处理模块终止。
exception inheritance
- 我们的错误类型也可以采用继承一个父亲类型
- 当我们抓取异常的时候,我们最好先catch子类,因为即使我们抛出的错误是子类,但是可以向上造型变成父类,我们的父类catch能够正确匹配。
stack unwinding
- 栈展开机制(Stack Unwinding)当 throw 抛出一个异常时,C++ 会从当前作用域开始向上回退,直到找到匹配的 catch 块。
- 在这个过程中:所有局部变量(自动变量)都会按照创建顺序的反序调用析构函数。如果对象是在堆上分配的(使用 new),不会自动调用析构函数,除非你手动调用了 delete。
Failure in constructors
- 如果constructor的时候失败,抛出了异常:
- 析构函数不会被调用
- 要人工清理已经分配的内存,因为析构函数不会调用,所以可能产生内存泄漏
解决上述问题
- 采用两段式,将内存分配的代码放在另一个init函数中,这样构造函数就不会失败,我们的析构函数也能顺利调用。
- 采用智能指针,即使抛出异常,展开机制仍然会调用智能指针的析构函数,避免内存泄漏
exceptions and destructors
- 当我们抛出异常后根据前面的机制会调用析构函数。
- 但是析构函数不能抛出异常:因为在栈展开时会调用析构函数,如果析构函数本身又抛出异常,C++无法处理,程序崩溃。
uncaught exceptions
- 如果异常抛出但是没有被caught,那么程序就会崩溃。
week 13–smart pointer
week 14–miscellaneous topics
named casts
- 想要类型转化,use a named cast:
static_cast,不容易犯错,在编译时期完成,可以进行数值类型转换,类层次转换,但不允许无关类型的指针转换,也不允许移除const。
dynamic_cast: 是一个强制类型转换操作符,主要用于多态基类的指针或引用与派生类指针或引用之间的转换,它是在程序运行时刻执行的。
- 向上转换:在类的继承层次结构中,从派生类向基类方向
的转换,即把派生类对象的指针或引用转换成基类对象的指
针或引用,这种转换常用C++的默认方式完成。 - 向下转换:当基类指针或引用实际派生类对象上时,向下强制转换能够成功。如果失败抛出null指针。如果是引用的转换失败则抛出异常
1
2
3
4D d,*pd
B *pb=&d, &rb=d; // 向上转换
pd=dynamic_cast<D*>(pb);// 向下转换
D &rd=dynamic_cast<D&> rb;// 向下转换- 向上转换:在类的继承层次结构中,从派生类向基类方向
reinterpret_cast,在编译时期完成,不需要做任何类型检查
const_cast,在编译时期完成,唯一能安全移除const的C++转换操作符。
example
1 |
|
涉及继承
1 |
|
multiple inheritance
example of ml
1 |
|
namespace
ambiguities
example of ambiguities
1 |
|
namespace aliases
- namespcace太短可能导致clash,但是太长可能不方便使用,我们可以使用命名空间别名,为现有命名空间创建一个新名称。
namespace composition
namespace are open
homework explanation
hw3.5
hw3.5,write the output of the following code
1 |
|
- 解释:这道题目主要考察子类和父类的构造顺序,以及对于一个结构体结构体内部元素的构造与整个结构体的构造顺序。
我们在main函数中构造一个Child对象,首先我们要构造基类对象,即构造Parent,但是又要首先将X构造完毕,才能成功构造Parent,构造完成基类以后,对于派生类,先构造自己的成员变量Y,再成功构造派生类对象。最后的析构顺序与构造顺序相反。
hw4.5
hw4.5,write the output of the following code
1 |
|
- 解释:
- 从宏观来看,我们首先定义了基类A,然后定义了子类B,B继承自A。
- 对A类进行分析:
- A类中有一个构造函数,输入i赋值为mi中。
- A类中有一个拷贝构造函数,输入rhs赋值给mi中。
- 还定义了一个赋值操作符,输入rhs赋值给mi中。
- 还定义了一个f虚函数,输出mi的值。
- 对B类进行分析:
- B类中有一个构造函数,输入i和j分别赋值给mi和mj中。
- 还定义了一个f函数,输出mi和mj的值。
- 分析主程序:首先定义两个对象a1和b;然后我们定义A类型的ra引用b,然而b是B类型发生向上转型(此时,ra 只能看到 A 类的接口部分(即 mi 和 f() 方法),而无法直接访问 B 类特有的成员变量 mj),但是在调用虚函数时会调用实际对象的派生类的方法,所以输出B::f(), 3, 4;接下来调用A的赋值操作符,则mi被更新为1,并输出A::operator=(),再次调用虚函数,输出B::f(), 1, 4;我们此时再调用A类中的拷贝构造函数(其实不用自己书写,编译器已经自动生成了),输出A::A(&),再调用A的f函数,输出A::f(), 1;
history final exam
2017-2018
Multiple choice
2.5
解释:这一道题主要是编译错误和运行时错误的理解。编译错误是指在编译阶段就出现的错误:语法错误、类型不匹配、未声明的变量等。而运行时错误是指代码已经开始运行了,但是程序执行过程中出现的错误:数组越界、除以零、无效指针访问、资源不可用等。再对这一道题分析,我们虽然没有显式声明vector的类型,但是这个不会出现报错,问题是现在vector并没有任何元素,长度为零,所以在运行时我们根本不可能通过下标访问到vector[0]
2.7
解释:const成员必须在构造函数的成员初始化列表中初始化,不能在构造函数体内赋值。