Lane
面向对象程序设计

面向对象程序设计

引言

本笔记基于cx老师的上课记录与xuan的笔记 感谢老师与前辈!

导航

week 1

C++的介绍

the first c++ programme

1
2
3
4
5
6
7
8
9
# include <iostream>
using namespace std;

int main()
{
cout <<"Hello,World! I am "<< 18
<<" Today!"<< endl;//endl理解为回车
return 0;
}
  • 注意:一般规范书写还是要返回

read input

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main()
{
cout << "Please enter your age: ";
int age;
cin >> age;
cout << "Hello ,World! I am" << age <<" years old!"<< endl;
return 0;
}

Using Objects

The string class

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39

//在程序的开头引用头文件
#include <string>
//定义string变量
string str;
//初始化
string str = "Hello";
//读和写
cin >> str;
cout << str;
//assignment
char cstr1[20];
char cstr2[20]="jaguar";
string str1;
string str2= "panther";
cstr1=cstr2;//非法
str1= str2; //合法
//Concatenation
string str3;
str3= str1+ str2;
str1+=str2;
str1+= "a string literal";
// constructors(Ctors)
string (const char *cp, int len);
string (const string& s2, int pos);
string (const string& s2, int pos ,int len);
// sub-string
substr(int pos ,int len);
//Modification
assign(...);
insert(...);
insert(int pos,const string& s);
erase(...);
append(...);
replace(...);
replace (int pos, int len,const string& s);
//search
find (const string& s);

string class 的应用

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
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>
using namespace std;

int main()
{
string str1="foo";
string str2="bar";
string str3=str1+str2;
cout <<"str3 = "<<str3 <<endl;
str2+=str1;
cout <<"str2 = "<<str2 << endl;

str3 = "hello, china!";
string str4("hello, zju!");
//两种初始化方式
string str5(str3);//拷贝构造
string str6(str3, 7, 5);//从str的第7个字符开始,长度为5本质上与成员函数substr功能一致
cout <<"str4 = "<<str4 <<endl;
cout <<"str5 = "<<str5 <<endl;
cout <<"str6 = "<<str6 <<endl;

string str7 =str3.substr(7,5);
cout <<"str7 = " << str7 << endl;
string str8 = str3;
str8.replace(7,5,"hangzhou");
cout <<"str8 = "<< str8 <<endl;

str8.assign(10, 'A');
cout <<"str8 = "<< str8 <<endl;

string str9 = "hello, hangzhou city";
cout << "str9 = " << str9 << endl;
string str_to_find = "hangzhou";
cout << str9.find(str_to_find) << endl;
str9.replace(str9.find(str_to_find),str_to_find.length(),"beijing");
cout <<"str9 = "<< str9 <<endl;
}

File I/O

1
2
3
4
5
6
7
8
9
#include <ifstream> //read from file
#include <ofstream> //write to file

ofstream File1("C:\\test.txt");
File1 << "Hello world" << std::endl;

ifstream File2("C:\\test.txt");
std::string str;
File2 >> str;

I/O example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
string str1= "foo, bar!";
ofstream fout("out.txt");
fout << str1 << endl;

ifstream fin("out.txt");
string str2, str3;
fin >> str2 >> str3;
cout << "str2= " << str2 << endl;
cout << "str3= " << str3 << endl;
}

regex example(替换字符)

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<regex>
#include <string>
using namespace std;

int main()
{
string s ="hello,student@zju!";
regex re("a|e|i|o|u");
string s1 = regex_replace(s,re,"*");
cout << s << "\n" << s1 << "\n";
}

week 2

A Quick Tour of C++

  • 这一节课cx老师通过介绍排序算法引出c++中的模板、自定义类、类的继承,快速介绍了c++中的几个性质(cx老师也说如果这一节课的每个步骤完全弄懂,这门课不用来听了bushi
  • 首先用c++写了选择排序的程序,和C语言大差不差
  • 以下的例子介绍了C++中的在函数定义中显式声明模板参数。这样我们对于不同的变量类型都可以适用。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<iostream>
#include<string>
template<typename T>
int min_element(T arr[],int begin,int end)
{
int min_idx=begin;
for(int i=begin+1;i<end;i++)
{
if(arr[i]<arr[min_idx])
min_idx=i;
}
return min_idx;
}
template<typename T>
void swap(T a,T b)
{
T temp=a;
a=b;
b=temp;
}
template<typename T>
void selection_sort(T arr[],int n)
{
for(int i=0;i<n;i++)
{
int min_idx=min_element(arr,i,n);
if(min_idx!=i)
swap(arr[min_idx],arr[i]);
}
}
template<typename T>
void print_array(T arr[],int n)
{
for(int i=0;i<n;i++)
{
std::cout <<arr[i] << ' ';
}
}

int main()
{
std::string arr[]={"hello","boys","and","girls","zju"};
int n=sizeof(arr)/sizeof(arr[0]);
selection_sort(arr,n);
print_array(arr,n);
}
  • 设计结构体来排序

    1. 引用传递(&):直接访问原始对象,这样能方便我们不额外开辟一个空间,直接修改原始对象。(当然对于常量变量并不会修改原始对象)
    2. 值传递:会额外开辟一个原始对象的副本。
    3. 对于我们的选择排序对于我们自定义的变量,我们需要自己定义我们的比较规则与输出规则。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Triangle{
private:
double a,b,c;
double area, perimeter;
public:
Triangle(double a,double b,double c):a(a),b(b),c(c) {}
void calc_area(){
double p=(a+b+c)/2;
area=sqrt(p*(p-a)*(p-b)*(p-c));
}
void calc_perimeter(){
perimeter=a+b+c;
}
};
//初始化时
Triangle arr3[]={Triangle(2,3,4),Triangle(3,4,5)};
  • 我们思考是否有一个抽象类,类似于前面的抽象类型来管理多个性质与成员相同的类。这就涉及c++的继承
    1. 注意抽象类的定义,抽象类定义为纯虚函数,即没有函数体,只有函数声明,其子类必须实现该函数。子类实现的函数的值存在在抽象类的protected域中。
    2. 继承的语法为:class 子类:访问修饰符 父类{};
    3. 子类实现父类的纯虚函数时,子类必须使用override关键字来修饰。
  • 此时在main函数中我们通常涉及指针数组来方便操作对象
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
26
27
class Shape{
protected:
double area,perimeter;
public:
virtual void calc_area()=0;
virtual void calc_perimeter()=0;
};
class Rectangle:public Shape{
private:
double w,h;
// double area, perimeter;
public:
Rectangle(double w,double h):w(w),h(h) {}
void calc_area() override {
area= w*h;
}
void calc_perimeter() override {
perimeter=2*(w+h);
}
};
//main函数调用
Shape* arr[]={new Rectangle(2,3),new Rectangle(5,5),new Circle(3),new Triangle(2,3,4)};
for (Shape* s:arr)
{
s->calc_area();
s->calc_perimeter();
}
  • 我们又仿照前面的结构体,按照下面的代码想要输出我们刚刚得到的结果,但是发现无法访问area和perimeter。因为我们的shape类的成员变量area和perimeter是protected的,所以无法访问。
  • 我们可以在Shape类中定义一个输出函数并采取friend字段。
  • 我们却发现输出的流却像地址,我们回到我们的打印的模板,此时的T其实是Shape*,所以此时的arr的类型就是指针,所以输出的流便是地址。
  • 我们可以额外再写一个T抽象函数专门针对指针类型
  • 我们继续前进:修改shape类,添加一个虚拟函数,想要输出每个类的名字,同时每个子类都实现这个函数。但是我们会发现程序报错:
    1. 因为我们输出函数中的Shape变量前有const修饰,说明我们不能改变它的状态,所以我们在输出函数中调用name()函数是不可行的。
    2. 我们想要解决需要在shape类中对name()函数用const修饰。显式告诉编译器我们在函数中不会改变状态。
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
26
27
28
29
30
31
32
33
34
std:: ostream &operator <<(std::ostream& out,const Shape& s)
{
return out <<"("<<s.area<<","<<s.perimeter<<")";
}
//修改shape类
class Shape{
protected:
double area,perimeter;
public:
virtual void calc_area()=0;
virtual void calc_perimeter()=0;
friend std:: ostream &operator <<(std::ostream& out,const Shape&);
};
//添加print函数
template<typename T>
void print_array(T* arr[],int n)
{
for(int i=0;i<n;i++)
{
std::cout << *arr[i] << ' ';
}
}
//shape类中添加虚拟函数
virtual std::string name()=0;
//同时输出流也改变一下
std:: ostream& operator <<(std :: ostream& out,const Shape & s )
{
return out <<"("<<s.name()<<","<<s.area<<","<<s.perimeter<<")"
}
//修改虚拟函数以及各个子类实现的函数
virtual std::string name() const=0;
std:: string name() const override{
return "Rectangle";
}
  • 我们再回到这堂课的最初出发点:selection sort,我们现在想要根据面积和周长分别进行排序,那这样我们需要添加一个selection_sort函数和find_min函数,因为此时我们的接口多了一个自定义的比较函数。
    1. 在这里需要注意的是我们的自定义的比较函数不能直接访问Shape类的成员变量,我们需要在Shape类中定义get函数。
    2. 其实当我们定义好get函数后,我们可以将Shape类中的freind修饰的输出函数删除,我们的输出函数可以通过调用get函数而不需要访问Shape类的成员变量。
  • 除了单独书写一个比较函数,我们还可以在主函数调用时直接在主函数中传入比较函数。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//扩充的选择排序
template<typename T,typename Compare>
int min_element(T arr[],int begin,int end,Compare comp)
{
int min_idx=begin;
for(int i=begin+1;i<end;i++)
{
if(comp(arr[i],arr[min_idx]))
min_idx=i;
}
return min_idx;
}
template<typename T,typename Compare>
void selection_sort(T arr[],int n,Compare comp)
{
for(int i=0;i<n;i++)
{
int min_idx=min_element(arr,i,n,comp);
if(min_idx!=i)
swap(arr[min_idx],arr[i]);
}
}
//我们自定义的比较函数与修改的父类
class Shape{
protected:
double area,perimeter;
public:
virtual void calc_area()=0;
virtual void calc_perimeter()=0;
virtual std::string name() const=0;
friend std:: ostream &operator <<(std::ostream& out,const Shape&);
double get_area() const {return area;}
double get_perimeter() const {return perimeter;}
};
bool less_shape_area(Shape * s1,Shape * s2)
{
return s1->get_area()<s2->get_area();
}
//直接传入比较函数
selection_sort(arr,n,[](Shape* s1,Shape* s2){return s1->get_area()<s2->get_area();})

week 3

STL

What is STL

  • C++的标准模板库的一部分
  • 封装C++的数据结构与算法
  • 包含:
    1. 容器:class templates,common data structures
    2. 算法
    3. 迭代器:泛化的指针,在容器与算法间打交道

Why should I use STL

  • 节省时间与工作量
  • 增加程序可读性

Containers

  • 线性容器
    1. array(static),vector(dynamic)
    2. deque(double-ended queue)
    3. forward_list(signlely linked list),list(doubly linked list)
  • 关联性容器(本质上是用红黑树)
    1. set(collection of unique keys)
    2. map(collection of key-value pairs)
    3. multiset,multimap
  • Unordered associative
    1. hashed by keys
    2. unordered_set,unordered_map
    3. unordered_multiset,unordered_multimap
  • Adaptors
    1. stack
    2. queue
    3. priority_queue

vector example

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
26
27
#include <iostream>
#include <vector>
using namespace std;

int main()
{
//初始化
vector<int> evens {2,4,6,8};
//push_back
evens.push_back(20);
evens.push_back(22);
//insert,在指定位置插入5个10
evens.insert(evens.begin()+4,5,10);
//四种遍历方式,但是如果想要在指定范围内输出我们需要使用迭代器
for (int i=0;i<evens.size();i++)
cout << evens[i] <<" ";
cout<< endl;
for (vector<int> :: iterator it = evens.begin();it < evens.end();it++)
cout << *it << " ";
cout<< endl;
for(auto it = evens.begin();it < evens.end();it++)
cout << *it << " ";
cout << endl;
for(int e : evens)
cout << e << " ";
cout<< endl;
}

other containers

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# include <iostream>
#include <string>
#include <list>
#include <map>
using namespace std;

int main()
{
//list
list <string> s;
s.push_back("hello");
s.push_back("world");
s.push_back("stl");

list <string>::iterator p;
//需要注意的是我们遍历的时候用!=,因为我们的内存是分散的,比较迭代器的地址没有意义
for(p=s.begin();p!=s.end();p++)
cout << *p <<" ";
cout << endl;

//map
map<string,int> price_list;
price_list["apple"]=3;
price_list["orange"]=5;
price_list["banana"]=1;

for (const auto & pair:price_list)
cout<< "{" << pair.first<<":"<<pair.second<<"}"<<" ";
cout<<endl;

string item;
double total=0;
//如果输入的item在前面并不存在,我们会发现值会被悄悄插进map去且赋值为零
/*while(cin>>item)
total+=price_list[item];
*/
//注意我们使用的contains在编译时要使用c++20
while(cin>>item)
{
if(price_list.contains(item))
total+=price_list[item];
else
cout<<"Mistake"<<endl;
}
cout << total <<endl;
for (const auto & pair:price_list)
cout<< "{" << pair.first<<":"<<pair.second<<"}"<<" ";
cout<<endl;

//再举一个例子
map<string,int> word_map;
for(const auto & w : {"we","are","not","humans","we","are","robots","!!!!"})
word_map[w]++;
for (const auto& [word,count]:word_map)
cout << count <<" occurrence(s) of word"<<word<<endl;


}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <list>
#include <vector>
#include "infix_iterator.h"
using namespace std;
int main()
{
vector<int> v ={1,2,3,5};
reverse(v.begin(),v.end());
//但是此时u没有东西,程序不知道在哪里开始
//我们想要在copy时自动插入,我们换一个迭代器
vector<int> u;
// copy(v.begin(),v.end(),u.begin());
copy(v.begin(),v.end(),back_inserter(u));
copy(u.begin(),u.end(),ostream_iterator<int>(cout,","));
cout<<endl;

list<int> l;
//每次在头部插入,相当于又reversse了一次所以结果与最初相同
copy(v.begin(),v.end(),front_inserter(l));
copy(l.begin(),l.end(),ostream_iterator<int>(cout,","));
cout<<endl;
//要注意下面的初始化方式的含义是10个8
vector <int> t(10,8);
copy(v.begin(),v.end(),t.begin());
copy(t.begin(),t.end(),ostream_iterator<int>(cout,","));
//想要消掉最后的逗号,可以基于ostream_iterator自己实现一个迭代器
copy(t.begin(),t.end(),infix_ostream_iterator<int>(cout,","));
cout<<endl;
}

iterators

  • connect containers and algorithms
  • 后面的课会讲到

pitfalls

  • access safety

    1. accessing an element out of range
    2. use push_back() for dynamic expansion
    3. preallocate with constructor
    4. reallocate with resize()
  • silent insertion

    1. map<>中如果没有对应的pair,可能悄悄添加
    2. 通常用count() or contains()(基于c++20)来检查
  • size() on list<>

    1. my_list.size() might cost linear time before C++11
    2. Constant time guaranteed:my_list.empty()
  • invalid iterator

    1. using invalid iterator
    1
    2
    3
    4
    5
    6
    7
    list <int> L;
    list <int>:: iterator li;
    li=L.begin();
    L.erase(li);
    ++li;//wrong
    //我们需要重新调整
    li=L.erase(li);

week 4

memory model

what are these variables?

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
26
27
28
29
30
31
int i; //global vars
static int j; //static global vars

void f()
{
int k; //local vars
static int l; //static local vars

int *p= malloc(sizeof(int)); //allocated vars
}
//一个例子
#include <cstdlib>
#include <iostream>
using namespace std;

int globalx=100;
//我们可以发现存放的区域的位置
//全局变量和静态变量放在一个区域(data/code段)
//局部变量放在一个区域(stack)
//动态分配的又在一个区域(heap)
int main()
{
static int staticx=30;
int localx=3;
int *px=(int*)malloc(sizeof(int));
cout<<"&globalx="<<&globalx<<endl;
cout<<"&staticx="<<&staticx<<endl;
cout<<"&localx="<<&localx<<endl;
cout<<"&px="<<&px<<endl;
cout<<"px="<<px<<endl;
}

分配位置

1

不同的变量介绍

  • 全局变量(global)
    1. vars defined outside any functions
    2. can be shared btw .cpp files
    3. 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
    4. 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
2
string s ="hello";
string *ps = &s;
  • operators with pointers
    1. get address
    2. get the object
    3. call the function
  • two ways to access
    1. string s;
      1.1 s is the object itself
      1.2 at this line,object s is created and initialized
    2. string *ps;
      2.1 ps is a pointer to an object
      2.2 the object ps points to is not konwn yet

Reference

Defining references

  • references are a new data type in C++
  • type& refname =name;
    1. for ordinary variable definitions
    2. an initial value is required
  • type& refname;
    1. In parameter lists or member variables
    2. Binding defined by caller or constructor
1
2
3
char c;//a character
char* p = &c;//a pointer to a character
char& r =c;//a reference to a character

Rules of references

  • 引用变量创造时必须初始化
  • 初始化建立了binding,并且不能再重新和另一个变量绑定
1
2
void f (int& x);
f(y);//在函数被调用时初始化
  • 引用变量的本质就是给已经存在的变量多了个名字
  • non-const的reference不能绑定rvalue,引用的non-const的目标是lvalue(能放在等号左侧的表达式)。
1
2
void func (int &);
func(i*3);//Wrong!!
  • 悬空引用出现报错,引用必须绑定到生命周期足够长的对象上
1
2
3
4
5
int &h()
{
int q;
return q;//error!
}

Type restrictions

  • No references to references
  • No pointers to references,but reference to pointer is ok(指针变量是一个健全的类型可以独立存在)
1
2
int& * p;//illegal
void f(int*& p);//ok
  • No arrays of references

Dynamically allocated memory

Dynamic memory allocation

  • new expression
    new int;
    new Stash;
    new int[10];
  • delete expression
    delete p;
    delete [] p;
  • new与malloc的差异在于:new在动态分配内存的同时还通过构造函数初始化对象,我们下面的例子就说明了这点。
  • 同时对于数组的删除,我们可以发现删除的顺序是从后往前删除的。
    1. 注意对于数组的删除采取 delete [] p;但是如果我们写delete p,只能删除第一个元素。
  • new、delete和malloc、free不能混用。
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
26
27
28
29
30
31
32
33
34
#include <cstdlib>
#include <iostream>
using namespace std;
struct Student{
int id;
//构造函数
Student(){
id = 0;
cout <<"Student::Student()"<<endl;
}
//delete时会调用这个函数
~Student(){
cout <<"Student::~Student()"<<endl;
}
};
int main()
{
int *pa= new int(1024);
cout << *pa << endl;
int *parr =new int[10];
for(int i=0;i<10;i++)
parr[i]=i;
for(int i=0;i<10;i++)
cout << parr[i] << endl;
delete pa;
delete [] parr;

Student * psl=(Student*)malloc(sizeof(Student));
cout <<"ps1->id = " <<psl->id <<endl;
Student * psl2=new Student;
cout <<"ps2->id = " <<psl2->id <<endl;
free(psl);
delete psl2;
}
  • 下面的例子告诉我们new出来的东西一定要及时删干净,否则迟早占用完内存。并且内存不能释放两次。
  • 还需要区分被释放的空间和零指针NULL没有任何关系。也就是说NULL也占据了动态空间的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdlib>
#include <iostream>
#include <thread>
using namespace std;

void f()
{
int *p=new int[1000];
p[0]=2024;
p[1]=3;
p[2]=22;

cout <<"we are in f() "<<p <<endl;
}
int main()
{
while(true)
{
f();
this_thread::sleep_for(1s);
}
}

const

  • 初始化以后不能再更改值,但是可以使用
  • run-time constants(若在程序跑起来的时候才能确定常量的值,则可能报错)
1
2
3
4
5
6
7
const int class_size = 12;
int finalGrade[class_size];//ok

int x;
cin >> x;
const int size = x;
double classAverage[size];//error
  • pointers with const
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a[]  = {53,54,55};
int * const p = a;//p is const
*p =20;//ok
p++;//error

const int *p = a;//(*p) is const
*p = 20; //error
p++;//ok

//再区分一下
string s("Fred");
const string* p = &s;//(*p) is const
string const* p = &s;//(*p) is const
string *const p = &s;//p is const
  • String literals

    1. 对于初始化的字符串,其实本质上就是初始化的常量字符串,所以不能修改里面的字符。(如果从内存分布的底层逻辑来说,初始化字符串本质上是一个指针指向代码段上方的一个字符串,(并且同样的字符串在代码段只会存一份),所以我们并不能通过更改指针来得到想要的结果)
    2. 如果想要修改字符,初始化时应该使用字符数组
    3. 当我们使用指针初始化字符串时只能使用const进行修饰,否则会报错。
  • passing addresses

    1. 当对象较大时可以传递地址(采用指针或者引用)
    2. 传参时通常加const修饰,表示该参数不会被修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

struct Student{
int id;
char address[1000];
};

void foo(const Student * ps)
{
cout << ps->id <<endl;
cout << (*ps).id <<endl;
}
void bar(const Student &s)
{
cout << s.id <<endl;
}
int main()
{
Student s;
s.id=2;
foo(&s);
bar(s);
}

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;显式将默认构造函数deleted

  • member 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
    12
      class 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Count{
int s = 0;
public:
~Count();

Count(int s) { this->s = s; }
int getS(){
return s;
}
void sPlus(){
s++;
}
};
Count::~Count() { cout << this->s << " ";}
Count count5(555);//全局对象
static Count count6(666);//静态
Count count7(777);//全局

void f(){
static Count count9(999);
}

int main() {
Count *count1 = new Count(111);
Count *count2 = new Count(222);

Count count3(333);
Count count4(444);

f();

static Count count8(888);

delete(count1);

for(int i = 1; i <= 5; i++)
for(Count c(1); c.getS() <= i; c.sPlus());

return 0;
}

输出结果: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 struct中的静态变量

静态变量与普通成员变量的区别
特性 静态变量 (static) 普通成员变量
生命周期 整个程序运行期间 实例的生命周期
存储位置 全局数据区 实例的内存空间
共享性 所有实例共享 每个实例独立
访问方式 类名或实例访问 只能通过实例访问
初始化 必须在类外初始化 可以在构造函数中初始化

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头文件中,并不会引起重定义的问题。

week6–composition and inheritance

composition

组合的概念

  • 它通过将一个类的对象作为另一个类的成员变量来实现。相比于继承,组合强调“拥有关系”而非“是一种”。

  • 这种对象我们也叫做嵌入式对象

    • 嵌入式对象都会被初始化,如果没有显式初始化,则调用默认构造函数。但如果没有默认构造函数,就必须在现在的类的初始化列表中提供参数进行显式初始化。
    • 如果存在多个嵌入式对象,嵌入式对象的构造顺序由初始化顺序决定,而不会受到初始化列表顺序影响。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {
    public:
    A(int x) { } // 没有默认构造函数
    };

    class B {
    private:
    A a;
    public:
    B() : a(42) { } // 必须显式初始化
    };

inheritance

继承的概念

  • 它是类与类之间的关系,一个类可以继承另一个类。继承关系是“是”的关系。

继承的优势

  • 避免代码重复
  • 重复利用代码
  • 可维护性
  • 可扩展性

继承的示例以及访问控制

example

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};

// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};

int main(void)
{
Rectangle Rect;

Rect.setWidth(5);
Rect.setHeight(7);

// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;

return 0;
}
  • 成员变量的访问权限
    oop1

  • 继承类型

    • 当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型
    • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
    • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
    • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
  • 派生类能够继承什么呢?

    • 一个派生类继承了所有的基类方法,但下列情况除外:
    • 基类的构造函数、析构函数和拷贝构造函数。
    • 基类的重载运算符。
    • 基类的友元函数

week8

8.1 拷贝构造函数

  • 处理形如Matrix m =m1;的初始化,C++引入了拷贝构造函数,它是一种特殊的构造函数。
  • 具体格式:
    • 对于Class T,拷贝构造函数的第一个参数是const T&或者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
      4
        T create() {
      return T(); // 纯右值:直接构造到调用处的目标地址(C++17 强制优化)
      }
      T obj = create(); // 无临时对象,直接构造 obj
    • 初始化对象时的临时物化省略

      • 用纯右值直接初始化另一个同类型对象
      • 例子
      1
      2
      3
      T obj = T();  // 直接构造 obj,无临时对象(C++17 强制优化)
      void foo(T t);
      foo(T()); // 直接构造参数 t,无临时对象

8.2 Special Member Functions

  • 我们前面提及的默认的构造函数,拷贝构造函数等统称为特殊成员函数。它们的共同特点是,如果没有用户显式声明的版本,编译器会生成默认的声明;如果需要使用,则编译器生成默认的定义。

Rule of Three

  • 如果用户需要自定义一个类的拷贝构造、拷贝赋值或者析构函数,那么基本上这三个都是必要的,也就是说用户都需要自己定义。

8.3 拷贝构造函数与赋值函数的例子

  • 总结,析构函数,拷贝构造函数,赋值操作符这三个函数如果系统生成有误,那么我们自己需要书写
  • 通常涉及指针操作时,此时系统自动生成的函数会存在问题,就如同下面的例子
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<cstring>
#include<iostream>
using namespace std;
struct Person {
string name;
Person(const char *s):name(s){
cout<<"Person()"<<endl;
}
Person(const Person& other)
{
cout<<"Person(&)"<<endl;
}
//char * name;
/*string name;//这种情况下不需要单独写拷贝构造函数
Person(const char *s):name(s){}*/
/*void init(const char* s)
{
name = new char[strlen(s)+1];
strcpy(name,s);
}
Person(const char* s)
{
init(s);
}
Person(const Person& other){
init(other.name);
cout<<"Person(&)"<<endl;
}
~Person(){
delete [] name;
}
Person& operator=(const Person& other)
{
//name=other.name;这是编译器自动书写的,但是会发生内存泄漏,原来的不会进行析构操作
//但是如果出现p2=p2的赋值操作仍然可能出现报错,我们进行如下修改
if(this!=&other){
delete []name;
init(other.name);
}
cout<<"Oprator=()"<<endl;
return *this;
}*/
};
Person foo(Person p){
cout<<"foo()"<<endl;
return p;//拷贝构造函数被调用
}
Person bar(const char* s)
{
cout<<"bar()"<<endl;
return Person(s);//作为临时值不需要进行拷贝构造
}
int main()
{
/*Person p1("Trump");
Person p2=p1;//在编译器自己生成的拷贝函数下,copy以后两个对象都是这块内存的拥有者
//这种需要我们自己写的拷贝函数的情况大多存在于析构函数也需要我们自己写时或者使用指针时
/*p2=p1;//这个操作是赋值操作,并不是拷贝构造
cout<< (void*)p1.name<<endl;
cout<< (void*)p2.name<<endl;*/
Person p1=foo("Trump");
cout<<"--------"<<endl;
Person p2=bar("Biden");

}

8.4 在容器中的拷贝函数

  • 因为我们的vector在不断扩容(在vector库中定义的是每次扩容两倍),所以会存在额外的copy操作,如果我们指定vector大小就可以避免额外的copy操作产生
  • 如果我们使用emplace_back,直接在容器内部构造对象就可以避免额外copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<vector>
using namespace std;
struct Point{
Point(int x,int y):x(x),y(y) {
cout<<"Point(int,int)"<<endl;
}
Point(const Point& p):x(p.x),y(p.y){
cout<<"copy a point"<<endl;
}
int x,y;
};
ostream& operator<<(ostream& out,const Point& p){
return out<<"("<<p.x<<","<<p.y<<")";
}
int main(){
vector<Point> pts;
pts.push_back(Point(1,2));
pts.push_back(Point(3,4));
pts.push_back(Point(5,6));
for(const Point& p:pts)
cout<<p<<endl;
}

week9–overloaded operators

  • 重载运算符,让用户定义的运算符具有运算能力
  • 调用函数的另一种方式

9.1 拷贝赋值运算符

  • 引入:对于代码c1 = c2;对于两个类的实例,如果实现这个操作以后,那么两个容器现在指向同一块内存,这样的操作其实有点类似于引用,并没有真正构造一个副本。
  • 为了解决这个问题,C++允许用户重载赋值运算符。

9.2 Restrictions

  • 不能重载不存在的运算符
  • 优先级和结合律是不变的,并且原运算符的参数数量不能发生改变
  • Operators must be overloaded on a class or enumeration type

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<vector>
#include<functional>
using namespace std;

void transform(vector<int>& v,function<int(int)> f)
{
for(int &x:v)
x=f(x);
}
class mul_by{
public:
mul_by(int a):a(a){}
int operator()(int x) const {
return x*a;
}
private:
int a;
};
int main()
{
vector<int> v{1,2,3,4,5};
transform(v,[](int x){return x*5;});

int a=5;
transform(v,[a](int x){return x*a;});
//因为我的mul_by定义了operator,重载了函数调用运算符。当创建对象后,就可以通过该对象直接调用operator(),所以能够实现以下操作
transform(v,mul_by(5));

for(int x:v)
cout<<x<<" ";
}

9.4 Type conversion

  • 想将 T 转化为 C, 那么需要一个 C(T) 的不加 explicit的构造函数,或者 operator C() 的重载。如果两个都有,编译器会出错。

week10–template

  • 模板编程属于泛型编程,把变量的类型当作参数来声明。
  • 多文件,模板一定要放到.h文件中,模板只是声明。

function templates

1
2
3
4
5
6
7
template<class T>
void swap(T& a,T& b)
{
T temp=a;
a=b;
b=temp;
}
  • template下面的内容就是模板,这里就是函数模板
  • T是模板参数,T是类型,T&是引用类型,T*是指针类型。class means any built-in type or user-defined type.

template instantiation

1
2
float a=1.2;float b=2.3;
swap(a,b);
  • 这里swap会调用函数模板,随后生成float swap,编译器会插入函数并调用。并且C++编译后的重载函数的名字会与原来有所不同,会把函数的参数类型编入函数的名字。
  • 模板函数不能同时实例化多个类型.swap(int,double) //error
  • 并且普通函数的优先级高于模板函数,编译器首先寻找普通函数,如果普通函数存在,则调用普通函数,否则调用模板函数。

class templates

1
2
3
4
5
6
7
8
9
10
11
12
template <class T> 
class Vector {
public:
Vector(int);
~Vector();
Vector(const Vector&);
Vector& operator=(const Vector&);
T& operator[](int);
private:
T* m_elements;
int m_size;
};
  • 类模板里的函数都是函数模板
  • 成员函数的定义需要模板(注意,类模板的函数是声明不是定义),就如同下面的例子,一定要加上vector<T>::
1
2
3
4
5
6
7
8
9
10
11
template <class T> 
Vector<T>::Vector(int size) : m_size(size) {
m_elements = new T[m_size];
}
template <class T>
T& Vector<T>::operator[](int indx) {
if (indx < m_size && indx > 0) { return m_elements[indx];
} else {
...
}
}
  • templates 能够使用多种类型
1
2
3
4
template <class T,class U> 
class Pair {
void install(const T& t,const U& u)
}
  • template嵌套:vector<vector<double *>>

模板相关的继承

  • 模板类可以继承非模板类,也可以继承模板类(需要实例化)

    • 模板类直接继承一个普通的非模板类
    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

  • 模板中可以声明一些常数,这些常数在模板中声明,在模板函数中可以访问。

week11–iterators

week12–exceptions

example

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
26
template <typename T>
class Vector{
private:
T* m_elements;
int m_size;
public:
Vector(int size = 0): m_size(size){
~Vector(){delete[] m_elements;}
int length() const{return m_size;}
T& operator[](int index){}
}
};

//return a special value reprensenting an error
if(idx < 0 || idx >= m_size)
{
T error_maker("error");
return error_maker;
}
//just die
if(idx < 0 || idx >= m_size)
exit(22);
return m_elements[idx];
//die but gracefully
assert(idx >= 0 && idx < m_size);
return m_elements[idx];

When to use exceptions

  • many times,yo don’t know what to do
  • anything can go wrong

how to raise an exception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class T>
T& Vector<T>::operator[](int idx){
if(idx < 0 || idx >= m_size)
throw VectorIndexError(idx); //我们自定义一个异常类型
return m_elements[idx];
}
class VectorIndexError{
public:
VectorIndexError(int idx):m_badvalue(idx){}
~VectorIndexError(){}
void diagnostic(){
cerr << "bad index: " << m_badvalue << endl;
}
private:
int m_badvalue;
}

What about your caller?

1
2
3
4
5
6
7
8
9
void outer(){
try{
func();
func2();
} catch (VectorIndexError& e){
e.diagnostic();
}
}
//我们使用catch捕获throw的异常
  • Mildly interested
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void outer2(){
string err_msg("Excpetion caught");
try{
func();
} catch (VectorIndexError&){
cout << err_msg << endl;
throw;//再传递异常
}
}
void outer3(){
try{
outer2();
} catch(...){
//捕获所有exceptions
cout << "Exception caught" << endl;
}
}

exception handlers

  • select exception by type
  • can re-raise
  • two forms
    • catch (SomeType v){//handler code}
    • catch (...){//handler code}

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

homework explanation

hw3.5

hw3.5,write the output of the following code

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

struct X {
X() {
std::cout << "X::X()" << std::endl;
}
~X() {
std::cout << "X::~X()" << std::endl;
}
};

struct Y {
Y() {
std::cout << "Y::Y()" << std::endl;
}
~Y() {
std::cout << "Y::~Y()" << std::endl;
}
};

struct Parent {
Parent() {
std::cout << "Parent::Parent()" << std::endl;
}
~Parent() {
std::cout << "Parent::~Parent()" << std::endl;
}
X x;
};

struct Child : public Parent {
Child() {
std::cout << "Child::Child()" << std::endl;
}
~Child() {
std::cout << "Child::~Child()" << std::endl;
}
Y y;
};

int main() {
Child c;
}
  • 解释:这道题目主要考察子类和父类的构造顺序,以及对于一个结构体结构体内部元素的构造与整个结构体的构造顺序。
    我们在main函数中构造一个Child对象,首先我们要构造基类对象,即构造Parent,但是又要首先将X构造完毕,才能成功构造Parent,构造完成基类以后,对于派生类,先构造自己的成员变量Y,再成功构造派生类对象。最后的析构顺序与构造顺序相反。

hw4.5

hw4.5,write the output of the following code

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;

class A
{
public:
A(int i) : mi(i) {}
A(const A& rhs) : mi(rhs.mi)
{
cout << "A::A(&)" << endl;
}
A& operator=(const A&rhs)
{
mi = rhs.mi;
cout << "A::operator=()" << endl;
return *this;
}
virtual void f()
{
cout << "A::f(), " << mi << endl;
}
protected:
int mi;
};

class B : public A
{
public:
B(int i, int j) : A(i), mj(j) {}
void f() override
{
cout << "B::f(), " << mi << ", " << mj << endl;
}
private:
int mj;
};

int main()
{
A a1(1);
B b(3,4);

A& ra = b;
ra.f();
ra = a1;
ra.f();

A a2 = b;
a2.f();
}
  • 解释:
    • 从宏观来看,我们首先定义了基类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;
本文作者:Lane
本文链接:https://lakerswillwin.github.io/2025/01/14/oop/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可