Reference: wk智云回放 CS 106L学不明白了(悲)
Lec 1
:::info
string
在C++里是一个类,区别于int
等name.length()
可以计算字符串的长度name + "Kenshin"
是strcat
的功能name.substr(pos, n)
,第一个参数是起始位置,第二个是字符数- 内存分配
new``delete
int* p = new int;``int* q = new int[10];
delete p;``delete[] q;
:::
Lec 2
Reference
并不是oop独有的概念
:::info
char& r = c; //initialize
本地变量的引用必须在声明时初始化- 引用可以看做别名
- 这种别名的绑定在程序运行期间是一直存在的且不可更改的
- 函数传入引用作为参数
void func(type& var);
- Restrictions
- No reference to reference
- No pointer to reference
- Reference to pointer is OK
- No array of references
- 在一些函数里,我们并不希望传入的指针所指向的变量被函数所影响,于是我们可以这样写
void func(**const **type* p);
声明和定义
类的概念
this
指针引入- wk:“所有的普通的成员函数都有一个隐藏的参数,是它参数表里的第一项,是c++编译器的语法糖,而这个参数就叫
this
,是这个结构体的指针”。 - 因此,使用
.
运算符的时候,a.Print()
实际上是Print(&a)
- wk:“所有的普通的成员函数都有一个隐藏的参数,是它参数表里的第一项,是c++编译器的语法糖,而这个参数就叫
class
和struct
基本一样,唯一的不同在于访问权限- 在C++中,可以使用
private:
和public:
来标识哪些是私有的,哪些是公开的- 一般来说,数据成员是私有的,方法是公开的
::
范围解析运算符 ( resolver )<class name>::<func name>
::<func name>
对象是属性+服务 ::: :::info
类和对象的关系:类是对象的抽象,对象是类的实例 ::: 这是一个简单的例子 ```cpp
include
using namespace std;
class Position {
private:
pair
void Position::printPos() { cout << “Position is: “; cout << “(“ << this->pos.first << “, “ << this->pos.second << “)” << endl; }
void Position::updatePos(pair
int main() {
Position a;
a.updatePos(pair
//输出 //Position is: (1, 2)
<a name="i9sAB"></a>
### `::`resolver
:::info
- `<class name>::<func name>`表示`func name`不自由,属于`class name`
- `::<func name>`表示`func name`是全局变量中的自由函数
- 变量同理
:::
:::info
- 类的静态变量 ( static )**属于类**而**不属于实例**,不能通过一般的方式初始化,可以通过`::`在全局范围内初始化。在访问静态变量时,始终推荐使用`class name::staticVar`的方式访问静态变量_(_[_参考了xx_](https://www.yuque.com/xianyuxuan/coding/cpp-oop#5TeQ9)_)_
:::
```cpp
#include <iostream>
using namespace std;
class Position {
private:
//static int type = 1; //不能把静态变量的初始化放置在变量定义中
static int type;
public:
static int getType(){return type;}
};
int Position::type = 1; //初始化静态变量,不能加static!
int main() {
Position a;
//cout << a.getType() << endl; //实际上转化为了下面对类的访问
cout << Position::getType() << endl;
}
Stash
:::info
- Container
- 存放对象的容器
- Stash一般是指typeless的container,可以扩大 :::
Lec 3
Class & Object
:::info
- OOP Characteristics
- A program is a bunch of objects telling each other what to do by sending messages
- 是“做什么”而不是“怎么做”
- 是发送请求“要做什么”而不是“好为人师”的指点
- “其实oop的核心就是封装,继承,多态”
:::
C’tor & D’tor | 构造函数 & 析构函数
成员函数中比较特殊的是C’tor ( constructor ) 和D’tor ( destructor )C’tor
```cpp class X{ private: int x, y; public: X(int x, int y){ this->x = x; this->y = y; } //其他方法 };
- A program is a bunch of objects telling each other what to do by sending messages
int main(){ X a(1,1); //a.X(1,1) Wrong!!! }
:::info
- 在语法上,要求构造函数名和类的名字**完全相同**,且构造函数没有返回值
- 构造函数在对象被创建时被编译器**隐式调用**,**不能被显式调用_(如line14)_**
- 称不需要参数的构造函数为Default C'tor
- 当构造函数未被声明时,存在一个不做任何事的 "Auto Default C'tor",我们认为C++的对象必须存在一个构造函数
- 我们希望每个类都存在一个Default C'tor
- Default C'tor 和 有参数的 C'tor 可以共存
- <eg>`class_name(type var){}`
- <eg>`class_name(){}`
:::
下面是初始化的例子
```cpp
#include <iostream>
using namespace std;
class Position {
private:
int x;
int y;
public:
Position(int x,int y) {
this->x = x;
this->y = y;
}
};
int main() {
//general initlization with C'tor
Position a[]{Position(1, 2), Position(3, 4)};
Position b{Position(1,1)};
}
D’tor
:::info
- 析构函数不能有任何参数
- 在对象的生命周期即将结束时,析构函数被隐式调用
- 通常在析构函数里做的事情是处理除内存以外的资源,如关闭打开的文件
:::
```cpp
include
using namespace std;
class Test { int id; public: Test(int i) { id = i; } ~Test() { cout << “ID: “ << id << “ destruction function is invoked!” << endl; }; };
int main() {
Test t0(0); //栈中分配
Test t1[3]{1, 2, 3}; //栈中分配,数组型对象
Test t2 = new Test(4); //堆中分配
delete t2;
Test t3 = new Test[3]{5, 6, 7}; //堆中分配
delete[]t3;
cout << “———End of Main———-“ << endl;
return 0;
}
/ ID: 4 destruction function is invoked! ID: 7 destruction function is invoked! ID: 6 destruction function is invoked! ID: 5 destruction function is invoked! ———End of Main———- ID: 3 destruction function is invoked! ID: 2 destruction function is invoked! ID: 1 destruction function is invoked! ID: 0 destruction function is invoked! /
<a name="MfM2h"></a>
### Some Tips
<a name="NH6qo"></a>
#### Definition of Class
:::info
- 声明放在`.h`里,函数的主体放在`.cpp`里
- 在类外定义时,需要范围解析运算符`::`,方法参见[这里](#i9sAB)
- 在类内定义时,函数默认被`inline`**内联函数**所修饰 [🔗**_( detail )_**](https://www.yuque.com/xianyuxuan/coding/cpp-oop#1wQNV)
- 主要作用是在函数优化上,是**空间换时间的做法**,适用于短小且频繁使用的代码,编译器会将函数内容贴在调用函数的地方来减少栈消耗
- 一般不采用这种写法
- **Compile unit**
- A `.cpp`file is a compile unit
:::
<a name="YJM0c"></a>
#### 标准头文件结构
```cpp
#ifdef HEADER_NAME
#define HEADER_NAME
//...
#endif //HEADER_NAME
:::info
- Abstract | 抽象
- “拨云见日”的能力——ignore details and focus on high level
- “庖丁解牛”的能力——dividing a whole into well-defined parts
- TDD | 测试驱动开发 :::
Lec 4
Container | 容器
:::info
- STL | Standard Template Library
Lec 5
:::info
- 初始化列表
- 初始化列表的执行顺序是成员变量的声明顺序,
type T1,T2,T3;``class_name:T3(1),T2(3),T1(2){}
的执行顺序是T1->T2->T3
- 成员变量初始化方法(按执行先后)(如下程序段)
- 构造函数参数表
- 定义时赋值
- 构造函数函数内部
Student::Student(string s):name(s){}
——initializationStudent::Student(string s){name = s;}
——assignment ,这两者的性能是不一样的
- 初始化列表的执行顺序是成员变量的声明顺序,
string flag{“Global Var”};
class Test { private: string flag = “2”; public: Test() : flag(“1”) { this->flag = “3”; } void Print() { cout << flag << endl; } };
int main() { Test t; t.Print(); return 0; }
//输出 / 3 //如果注释掉第13行,输出1, //如果取消第12行的参数列表,输出2 /
<a name="Clx1P"></a>
## Overload | 重载
:::info
- **Overload | 重载**
- `void print(string str);`
- `void print(double r, int width);`
- etc
- **函数名可以相同**但**参数表不同**
- 重载**不支持**自动类型转换
- `void func(int a)``void func(double a)`,调用`func(1.1)`**不会**取整为1
:::
<a name="OthXp"></a>
## Default Argument | 默认参数
:::info
- format
- `void func(int a = 1);`
- `void func(int a, int b, int c = 1);`
- `void func(int a, int c = 1, int b);`非法
- 省略参数时,按default argument赋值,传入参数时,按传入参数赋值
- **有**默认值的参数必须位于**没有默**认值参数**之后**
- 若同时有函数声明和者函数体,默认参数只能写在其中一个里
:::
<a name="PUlhY"></a>
## C++ Access Control
:::info
- C++通过标签来控制权限
- `public`公有成员
- `private`私有成员
- `protected`受保护的成员
- `friend`友元
:::
<a name="kD9e3"></a>
### `protected`
:::info
- 没有继承的情况下,`protected`和`private`相同
- `protected`的可访问范围比`private`大,比`public`小
:::
![](https://cdn.nlark.com/yuque/0/2022/jpeg/22181361/1642687310207-e9d00fa9-0210-4000-801b-f5c6b222ffe0.jpeg)
:::info
- 基类的`protected`可以在派生类的**作用域内**被访问
- [🔗](http://c.biancheng.net/view/252.html)详解
:::
```cpp
using namespace std;
class Base {
private:
int nPrivate;
public:
int nPublic;
protected:
int nProtected;
};
class Derived :public Base {
void AccessBase() {
nPublic = 1; //OK
//nPrivate = 1; //错,不能访问基类私有成员
nProtected = 1; //OK 访问从基类继承的protected成员
//上面三行访问的实际上是this指针所指向的对象
Derived a;
a.nProtected = 1; //OK
a.nPublic = 1; //OK
Base b;
//b.nProtected = 1; //错 此时并不在派生类作用域里
b.nPublic = 1; //OK
}
};
int main() {
Base c;
Derived d;
//c.nProtected = 1; //错 不在派生类作用域内
//d.nProtected = 1; //错 不在派生类作用域内
d.nPublic = 1; //OK
return 0;
}
friend
:::info
- 类的友元定义在类的外部,但有权访问类内的
private
和protected
- 友元可以是
- 函数
- 类,其所有成员皆是友元
- 友元关系不可传递,即类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元 ::: ```cpp class Student{ private: int id; friend class Grade;
public: friend void printID(Student student) { cout << student.id << endl; //可以访问private } };
class Grade{ Student student; public: void func() { student.id = 1; //可以访问private printID(student); //可以访问private } };
<a name="BCpAg"></a>
## Inline Function | 内联函数
> [🔗](#NH6qo)See Here
:::info
**内联函数inline:**引入内联函数的目的是为了解决程序中函数调用的效率问题。程序在编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要注意:
- 在内联函数内不允许使用循环语句和开关语句
- 内联函数的定义必须出现在内联函数第一次调用之前
- 类结构中内部定义的函数是内联函数
:::
:::warning
- 和宏的区别与联系
- 做法相似,但宏只是简单地文本替换,内联可以做函数做的事
- 内联函数的声明和定义必须在同一个编译单元里,否则无法编译
:::
<a name="FvVPK"></a>
# Lec 6
<a name="uc9h9"></a>
## `const`
<a name="YVpqy"></a>
### const variables & pointers
“C++对`const`的变量的保护只在编译时”
:::info
- 修饰局部变量
- `const int x = 1;``int const x = 1;`两种写法一样
- 修饰指针
- `const int* px = &a;``int const* px = &a;`**常量指针**
- 不能通过`*px = 1`的方式来改变指针指向的值
- 可以间接地通过引用的对象被改变`a = 1`
- 可以指向别的地址`px = &b`
- `int* const px = &a;`**指针常量**
- 指针常量指向的地址不能改变
- 可以通过这个指针来修改值`*p = 1`
- `const int* const px = &a;`**指向常量的常指针**
- 指向的地址不能改变,也不能通过这个指针来修改变量的值
- ⚠️区分**常量指针**和**指针常量**的关键在于`*`的位置,我们以`*`为分界线,如果`const`在`*`的**左边**,则为常量指针,如果`const`在`*`的**右边**则为指针常量。如果我们将`*`读作“指针”,将`const`读作“常量”的话,则`const int* px`是常量指针,`int const* px`是指针常量。
:::
大型结构作为参数传入函数时,更好的做法是传一个指针,为了避免错误的修改,用`const`修饰传入的指针<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22181361/1642748439892-750d2335-b91b-41be-9f83-bc9929db514c.png#clientId=u1e564561-58b8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=384&id=u0c971d3f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=767&originWidth=1143&originalType=binary&ratio=1&rotation=0&showTitle=false&size=383016&status=done&style=none&taskId=u52efca49-43c5-42b7-b83d-44ea44200fc&title=&width=571.5)
<a name="jmLk1"></a>
### const objects
当一个对象被`const`修饰时,只有不修改成员变量值的那些函数才可以被调用,C++的做法是使用`const`修饰这些函数<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22181361/1642748913439-46563283-f0d1-42b5-a5fd-e0e3d17f93a0.png#clientId=u1e564561-58b8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=245&id=u19a07ba0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=835&originWidth=1252&originalType=binary&ratio=1&rotation=0&showTitle=false&size=437838&status=done&style=none&taskId=u83849ec2-13cd-4f12-9910-8e3ffd8c4ed&title=&width=367)![image.png](https://cdn.nlark.com/yuque/0/2022/png/22181361/1642749002094-62cba629-4682-4fff-ab52-d173a30dd0f7.png#clientId=u1e564561-58b8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=245&id=u78363f8f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=858&originWidth=1303&originalType=binary&ratio=1&rotation=0&showTitle=false&size=563424&status=done&style=none&taskId=ubeb7dd24-3972-4d0d-9b14-aea5e162060&title=&width=372)
:::warning
- 声明和定义处都需要`const`修饰
- 即使用`const`修饰,编译器仍会检查函数内部是否修改了`const`对象的值或者调用了其他没有用`const`修饰的函数
- 用`const`修饰过的成员函数对于const objects更安全
:::
下面程序段是一个简单的例子
```cpp
class Student{
private:
int id;
public:
Student(){}
void print() const{
cout << "const_print" << endl;
}
void print(){
cout << "non_const_print" << endl;
}
};
int main() {
const Student student;
student.print(); //调用line6
Student student1;
student1.print(); //调用line9
}
/*
const_print
non_const_print
*/
:::info
- 若类的成员变量被
const
修饰时,变量必须在初始化列表时被初始化 :::
static
class Position {
private:
//static int type = 1; //不能把静态变量的初始化放置在变量定义中
static int type;
public:
static int getType(){return type;}
//return this->type //Wrong!!!
};
int Position::type = 1; //初始化静态变量,不能加static!
int main() {
Position a;
//cout << a.getType() << endl; //实际上转化为了下面对类的访问
cout << Position::getType() << endl;
}
类的静态变量
:::info
- 类的静态变量 ( static )属于类而不属于实例,不能通过一般的方式初始化,可以通过
::
在全局范围内初始化。在访问静态变量时,始终推荐使用class name::staticVar
的方式访问静态变量(参考了xx) .cpp
里不要使用static
( line 10 ),只在类里 (.h
)声明即可-
类的静态函数
:::info
类的静态函数不再有
this
指针,也不能直接访问非静态成员或非静态函数 ( line 7 ).cpp
里不要使用static
( 如line 10,对函数来说一样 ),只在类里 (.h
)声明即可 :::
namespace
& using
:::info
- 使用
namespace
来封装代码 ::: ```cpp //include in “xxx.h” namespace Animals{ class Cat{ public:
}; class Dog{void Meow(){cout << "Meow" << endl;}
}; int GetSpeciesNum(); void ListSpecies(); }//something
int main() { using namespace Animals; Animals::Dog myDog; Animals::Cat myCat; myCat.Meow(); Animals::ListSpecies(); }
:::info
- 使用`using namespace space_name`后,可以省略`space_name`,但要避免**歧义( Ambiguities )**
- 使用全名访问时总是可以的
:::
```cpp
namespace X{
void f();
void g();
}
namespace Y{
void g();
void h();
}
int main() {
using namespace X;
using namespace Y;
f();
//g(); //歧义
X::g(); //OK
h();
}
:::info
namespace
的组合( composition ),可以在namespace
的里面继续使用namespace
- 多个
namespace
同名,在编译时会被合为一个namespace
:::
Lec 7
Inheritance | 继承
“‘继承’是C++代码复用的一种手段,拿一个已有的类去定义新的类。区别于‘组合’在类里构造新对象”。
- “Inheritance is to take existing class, clone it, and then make additions and modifications to the clone”
- 从这个定义来看继承有“遗传”的意思,而modifications和additions可以理解为“变异”。
- “The ability to define the behavior or implementation of a class as a superset(超集)of another set”
“接口的重用——继承 (inheritance)。我们定义了“人类”这个类,有时我们需要更细致的划分。例如我们需要“学生”这样一个类。显然,“学生”是“人类”的真子集,因此“学生”这个类必然拥有“人类”的状态和行为。也就是说,“人类”具有的接口在“学生”中都能找到。而不同的是,“学生”作为一个更细的划分,具有一些“人类”不一定具有的状态和行为,例如均绩和交作业。“人类”是比“学生”更为抽象的一个概念。 那么,“学生”作为一个新的类,将“人类”的接口拷贝一份当然是可以达到要求的,但是这将大幅降低设计的效率和模型的可维护性。因此我们引入了继承这一机制。继承可以让我们克隆一个(或多个)已经存在的类的状态和行为,并在克隆的基础上进行一些增加或修改,从而获得我们需要的类。我们将原来的类称为基类、超类 或父类,新的类称为派生类、继承类 或子类”
基本用法
:::info
- 语法一般是
class DerivedClassName:access-specifier BaseClass1Name, access-specifier BaseClass2Name, ...
- 这里访问修饰符access-specifier是
private
,public
,protected
中的一个(🔗protected
访问范围) - 不加访问修饰符,则默认为
private
- BaseClass 是基类的名字,DerivedClass 是继承类的名字
- 这里访问修饰符access-specifier是
不同类型的继承
**public**
:基类的public
也是派生类的public
,基类的protected
也是派生类的protected
,基类的private
不能被派生类直接访问。但是可以通过调用基类的公有和保护成员来访问(即这个变量仍然存在于子类的内存中,但是不能由子类直接访问)**protected**
:基类的public
和protected
将成为派生类的protected
**private**
:基类的public
和protected
将成为派生类的private
我们几乎不使用 protected 或 private 继承,通常使用 public 继承 ::: ```cpp class Base { private: int privateVar; protected: int protectedVar; public: int publicVar;
Base() : privateVar(1), publicVar(2), protectedVar(3) {} void Print() { cout << “privateVar: “ << privateVar << endl; cout << “publicVar: “ << publicVar << endl; cout << “protectedVar: “ << protectedVar << endl; cout << “———-“ << endl; } void Modify() { srand(time(NULL)); privateVar = rand() % 10; publicVar = rand() % 10; protectedVar = rand() % 10; } };
class Derived:public Base { public: void DerivedModify(){ srand(time(NULL)); //privateVar = rand() % 10; //Wrong!!! 只能通过基类的函数访问private publicVar = rand() % 10; protectedVar = rand() % 10; } };
int main() { Base base; base.Print(); Derived derived; derived.Modify(); derived.Print(); derived.DerivedModify(); derived.Print(); }
:::info
- 子类**不继承父类的构造函数、析构函数和**[**🔗**](#NaAzp)**重载运算符**。在创建一个子类对象时,如果没有明确指出,则子类对象构造时会首先调用父类的构造函数
:::
```cpp
class Base {
public:
Base(){cout<< "Base()" << endl;}
~Base(){cout<< "~Base()" << endl;}
};
class Derived:public Base {
public:
Derived(){cout << "Derived()" << endl;}
~Derived(){cout << "~Derived()" << endl;}
};
int main() {
Derived derived;
}
/*
Base()
Derived()
~Derived()
~Base()
*/
:::info
- 在父类没有 Default C’tor 时,子类需要显式的初始化父类,可使用参数列表 (line9) ::: ```cpp class Base { public: Base(int x){cout<< “Base()” << endl;} //~Base(){cout<< “~Base()” << endl;} };
class Derived:public Base { public: Derived():Base(1){cout << “Derived()” << endl;} //~Derived(){cout << “~Derived()” << endl;} };
<a name="pAzc1"></a>
### Name Hide | 名字隐藏
:::info
- **名字隐藏**是指父类中有一组重载函数,子类在继承父类时如果覆盖了这组重载函数中的任意一个,则其余没有被覆盖的同名函数在子类中是不可见的
- **解决方案**
- 可以在子类中不使用覆盖函数,而是给子类的方法选择一个不同的函数名以区别于父类的方法
- 另一种解决方案是子类覆盖父类中所有的重载方法,虽然子类中有些方法的实现与父类完全一致,但是这样做的好处是不会增加新的函数名
:::
```cpp
class Base {
public:
void Print(){cout <<"void_print" << endl;}
void Print(string s){cout << "string_print: " << s << endl;}
void Print(double x){cout <<"double_print: " << x << endl;}
};
class Derived:public Base {
public:
void Print(string s){
Base::Print(s);
}
};
int main() {
Derived derived;
derived.Print("Hello World");
//derived.Print(1.1); //Wrong!!!
//derived.Print(); //Wrong!!!
}
🔗虚继承
先来看这样一段代码
class Base {
private:
int a = 1;
};
class Derived:public Base {
private:
int a = 2;
int b = 3;
public:
void Print(){cout << a << endl;}
};
int main() {
Derived derived;
int *ptr = (int *) &derived;
derived.Print();
cout <<"---" << endl;
for (int i = 0; i < sizeof(derived) / sizeof(int); i++) {
cout << ptr[i] << endl;
}
}
/*
2
---
1
2
3
*/
子类和父类中有同名的a
,但打印出的却是子类的a
,并且通过后续的打印看出子类中的确继承了父类的a
,这是为什么呢?
摘自xyx
向上整型 | upcast
class Base {...};
class Derived:public Base {...};
int main() {
Derived derived;
Base base = derived; //slice
Base *basePtr = &derived; //upcast
Base &baseRef = derived; //upcast
}
:::info
- 将子类的对象向上整型为一个父类的对象,这总是可以做到的,因为子类包含父类的所有对象和方法,但是子类特有的成员会丢失 :::
Lec 8
多态 | Polymorphism
虚函数 & 纯虚函数
class Shape{
protected:
Point center;
public:
virtual void Render() {} //dynmaic binding
void Move(){} //static binding
};
class Rectangle:public Shape {
protected:
int width, height;
public:
void virtual Render() { cout << "..." << endl << "..." << endl; }
};
class Square:public Shape {
public:
void virtual Render() { cout << ".." << endl << ".." << endl; }
};
int main() {
Rectangle r;
Square s;
Shape* ptr = &r;
ptr->Render();
}
/*
...
...
*/
:::info
- 虚函数是在基类中使用关键字
virtual
声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数 - 如例程,此时父类和子类的
Render()
存在 Override 的关系- 参数表要相同才可构成 Override 关系
- Name Hide 依旧存在,子类需要 Override 父类的所有重载函数 :::
“我们可以通过 virtual Type functionName( Parameter List ) = 0; 来定义一个 纯虚函数。当我们想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数”
:::info
- 一旦类中有一个纯虚函数 ( pure virtual function ),这个类就成为了抽象类。抽象类不能创建对象 :::
🔗How virtual works
:::info
- 每个有虚函数的类都有一个 v-ptr ,指向这个类的 v-table 。v-table 里记录了这个类里所有虚函数的地址(函数指针)
- v-ptr在构造函数里被唯一的初始化,在后续操作中均不改变
- 对虚函数的调用可以归结为
pObj->v_ptr->v_table[]
-
静态绑定 & 动态绑定
:::info
静态绑定 | static binding
- 发生在编译时刻,又称早绑定( early binding )
- 编译器能够明确知道是哪个类的方法
- 动态绑定 | dynamic binding
- 发生在运行时刻,又称晚绑定( late binding )
virtual
关键字声明且通过指针访问的为动态绑定 :::
接口(抽象类)
接上面,由于继承的存在,一些类可以用于去规范派生类的行为,而不用于实例化对象。这样的类被称为抽象类( Abstract Class ),也称为接口( Interface )。
Lec 9
拷贝构造 | Copy C’tor
:::info
- 形式上,拷贝构造函数是
class_name(**const** class_name& r){}
- 拷贝构造发生在如
**class_name obj1 = obj2;**``**func(obj);**``**class_name obj1 = func(obj2);**
的情况下。当传入参数为对象时,会在函数内部复制一个临时的对象,此时会发生拷贝构造。但要注意一点,在函数返回发生拷贝构造时要注意可能会发生优化,可能不会发生拷贝构造。隐式的拷贝构造还发生在将对象放入容器中时,如**vector<class_name> vec{obj1,obj2};**
- 区分拷贝构造和赋值的区别(只有C++有给对象赋值的概念,原因是这一定程度上违背了封装的原则)
- 当没有拷贝构造函数时,会调用默认拷贝构造函数,即直接复制所有成员。但对于指针来说会导致错误,如下图及例程
:::
class A{
char *str;
public:
//A(const A&){}
A(const char *s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
~A() {
delete[]str;
}
};
int main(){
A p1("Hello");
A p2 = p1; //发生拷贝构造
//调用析构函数,先delete p2再delete p1,
//但p1 p2指向的是同一块内存,发生错误
}
一个正确的例程如下:
static int counter = 0;
class Array{
int *pInt;
int size;
public:
Array() {
counter++;
size = 1;
pInt = new int;
cout << "default c'tor...address: " << &pInt
<< ", current obj: " << counter << endl;
}
Array(int size) {
counter++;
this->size = size;
pInt = new int(size);
cout << "c'tor...address: " << &pInt
<< ", current obj: " << counter << endl;
}
Array(const Array& obj) { //copy c'tor
counter++;
pInt = new int(obj.size);
*pInt = *(obj.pInt);
cout << "copy c'tor...address: " << &pInt
<< ", current obj: " << counter << endl;
}
~Array() {
counter--;
cout << "deleted...address: " << &pInt
<< ", current obj: " << counter << endl;
delete pInt;
}
};
Array& func(Array obj) {
cout << "--func invoked--" << endl;
cout << "--end of func--" << endl;
return obj;
}
int main(){
Array arr1;
Array arr2(2);
Array arr3(arr2);
func(arr2);
return 0;
}
/*
default c'tor...address: 0x61fdf0, current obj: 1
c'tor...address: 0x61fde0, current obj: 2
copy c'tor...address: 0x61fdd0, current obj: 3
copy c'tor...address: 0x61fe00, current obj: 4
--func invoked--
--end of func--
deleted...address: 0x61fe00, current obj: 3
deleted...address: 0x61fdd0, current obj: 2
deleted...address: 0x61fde0, current obj: 1
deleted...address: 0x61fdf0, current obj: 0
*/
:::success
- 可以看出 line 45 调用了拷贝构造函数
line 46 调用
func
后,由于传入参数为 Array 对象,也调用了拷贝构造函数,在函数返回后调用析构函数 :::Overload Operators | 重载运算符
可重载的运算符
:::success
Restrictions
- 只能重载已有运算符
- 只能在自定义的类(结构)中重载运算符
- 不能改变运算符的目数
- 不能改变优先级
:::
成员函数形式的运算符重载
```cpp class Point { double x, y; public: Point() {}; Point(double x, double y) { this->x = x, this->y = y; } const Point operator-(const Point &p) const { return Point(x - p.x, y - p.y); } const Point operator+(const Point &p) const { return Point(x + p.x, y + p.y); } void Print() { cout << “(“ << x << “,” << y << “)” << endl; } void Print(Point p) { cout << “(“ << p.x << “,” << p.y << “)” << endl; } };
int main() { Point p(1, 2);
p = p + Point(1,2);
p.Print();
p = p - p;
p.Print(p);
} / (2,4) (0,0) /
:::success
- 返回值为`const Point`是为了避免运算结果被当做左值使用,即避免`p + Point(1,2) = p;`的出现
- `+`为双目运算符,但参数只需要显式的写出一个,另一个为`this`
- 成员运算符重载后**存在隐式类型转换**,但一般一般以**运算符左侧(receiver)**的类型为目标类型
:::
<a name="eYTbA"></a>
### 全局函数形式的运算符重载
考虑到需要访问类的私有成员变量,一般全局函数形式的运算符重载需要在类里**声明为友元,**或者类已经提供了访问私有成员变量的方法。
```cpp
class Point {
double x, y;
public:
Point() {};
Point(double x, double y) {
this->x = x, this->y = y;
}
friend const Point operator-(const Point &left, const Point &right);
friend const Point operator+(const Point &left, const Point &right);
void Print() {
cout << "(" << x << "," << y << ")" << endl;
}
void Print(Point p) {
cout << "(" << p.x << "," << p.y << ")" << endl;
}
};
const Point operator-(const Point &left, const Point &right) {
return Point(left.x - right.x, left.y - right.y);
}
const Point operator+(const Point &left, const Point &right) {
return Point(left.x + right.x, left.y + right.y);
}
:::success
如line10, 11声明为友元,且操作数需要设置为两个 :::
运算符重载策略
:::success
单目( Unary )运算符尽量做成成员
**=**``**()**``**[]**``**->**``**->***
必须作为成员
- 双目运算符尽量做成全局
()
是指通过指针调用函数 :::
运算符重载规范
:::success
-
部分运算符原型
:::success
第三条
E
是指索引元素类型,T
是类。如vector<string> v;
,对于v[i]
,E
为string
,T
为vector<string>
这两个运算符既可以出现在操作数前 ( prefix ),又可以出现在操作数后 ( postfix )。为了区别这两种情况,编译器会在 postfix 时传递一个参数 (int) 0 作为标记 ::: :::success
++(*this);
不写为*this += 1;
的原因是,在以后修改成员变量时,只需要修改一处*this += 1;
,这样其他地方也被一并修改 :::关系运算符重载
:::success-
下标运算符重载
输入输出流重载
>>
&<<
:::success
必须是有两个参数的非成员函数
模板必须是
**istream& operator>>(istream& is, class_name& obj);**
,cin >> a >> b;
的实现其实就是cin >> a
的结果还是cin
,副作用是向a
写入一个值,因此可以“串起来”读入值 :::自定义控制符
// define your own manipulators
// skeleton for an output stream manipulator
ostream &manip(ostream &out){
// ... do something
return out;
}
//
operator
=
1 C++面向对象
:::success为了避免
obj = obj;
这样的赋值出现导致delete
出现错误 ::: :::danger到目前为止,我们一定要写的函数有:
:::success
上述情况会用
"abc"
制造PathName
的对象,然后再做赋值,但拷贝构造可能和重载的赋值运算符发生冲突。使用explicit
关键字允许拷贝构造而不允许以赋值形式的类型转换 ::: 更为通用的写法是:
:::success语法是
class_name1 operator class_name2()
- 将
class_name1
的对象转为class_name2
的对象
- 将
- 我们什么时候可以做
**T -> C**
?T
中有转化为C
的重载,即operator C()
存在C
中存在利用T
的构造,即C(T)
存在- 上述两种情况只能同时发生一种
- 由于两种不能同时存在,因此我们引入了
explicit
保证构造函数不被用于类型转换 :::
函数传参/返回值的Tips
Left Values & Right Values
右值引用
:::info
- 右值引用的目的是减少拷贝,加速运算 :::
Lec 11
Templates | 模板
Function Templates | 函数模板
template <class T>
void Swap(T &a,T &b) {
T temp = a;
a = b;
b = temp;
}
:::info
class
可以写作type
,T
也可以更换- 模板又称元 ( meta ) 代码,编译器会将模板实例化来产生新的代码
- 使用模板时,要求每个实例中的
T
必须一样 ::: ```cpp templatevoid Func(T &x, T &y){ //… }
//Func(1.0, ‘a’)会报错,模板要求T应一样
:::info
- 函数没有参数但内部会用到`T`时,可以在函数名的后面加上`<type>`来声明
:::
```cpp
template <class T>
void Func(void){
//...
}
//Func<int>();
Class Templates | 类模板
template <class T>
class MyVector {
private:
T *ptr;
int size;
public:
MyVector(const MyVector &); //copy c'tor
MyVector(int size); //c'tor
MyVector(); //default c'tor
virtual ~MyVector(); //d'tor
MyVector &operator=(const MyVector &); //operator =
};
:::info
- 模板中的成员函数在类的外部定义时,需要看做函数模板来定义,要声明类,用
**<>**
::: ```cpp templateMyVector ::MyVector(const MyVector &) { //… }
template
声明了一个类模板之后,可以这样使用:
```cpp
MyVector<int> v1(4);
MyVector<string> v2;
MyVector< MyVector<double> >;
:::warning
类模板往往需要重载相应的运算符,否则在编译时可以通过但链接时会报错 ::: :::info
多个类型
- 有非类型的参数
template
语句属于声明,需要放在.h
中- “一个类模板应该没有对应的.cpp文件才是对的,全部都在头文件里”
- “大部分情况下,类模板的函数都做成
inline
”
- 模板与继承
- 必须实例化模板才能继承
怎么写模板?
关于友元函数在类模板中声明的问题
这个问题初学的时候确实比较难发现:
Lec 12(摸了)
Exceptions | 异常
Lec 13(摸了)
Lec 14(摸了)
Lec 15
Streams | 流
:::info
- 一维、单向 :::