C++项目总结

IMG_2999(20210503-020300)

说起来是真的惨,经过近两周的抱佛脚式学习,已经被这个面对对象 C++ 折磨的菠萝菠萝大OMO,然而实际学会的东西真就屈指可数,甚至可以说是完全零提升,下次选课一定要慎重了。

一、类,对象与继承:

1、类

C++相比C语言,加入了面对对象编程,而类则是其核心特性,非常重要。

类中存在的数据和方法都称为类的成员。

那么类应该如何定义呢:

1
2
3
4
5
6
class classname
{
Access specifiers://访问修饰符:一般有 private/public/protected
Date members/variables;//变量
Member functions(){}//方法
};//分号结束对一个类的定义

这其实就是把所有相关的变量和方法都装到了一个盒子里——称之为数据封装

感觉有点像一个大结构体(多了函数和一些杂七杂八的东西)

内部任何变量或方法可以直接调用,外部就需要根据情况用 “ . ““->” 来进行调用。

补充:这里还有一个特殊的调用工具—— “ this ” 指针

它的作用就是返回当前这个对象自己的指针,在函数内部可以用来指向调用对象

访问修饰符:分成了 private / public / protected 三类,每次声明写一次就够了,后面直到遇到下一个访问修饰符为止都会归为刚刚声明的属性类型。

例:

1
2
3
4
5
6
7
8
class animal
{
public:
int num;
int species;//以上两个变量均为public
private:
int consume;
};
关于构造函数与析构函数:

这俩家伙一个是负责在你用这个类创建新的对象的时候进行,而且 什么类型都不会返回(包括void)

虽然不能返回类型,但是我们是可以传入参数并进行操作的:

1
2
3
4
5
classname::classname(int k)
{
variable=k;
cout<<"init successfully!"<<endl;
}//然后运行时,内部的一切操作都会在新对象建立时自动运行

更简便的,C++ 支持使用初始化列表来快速赋值:

1
2
3
4
classname::classname(int k,int l):variable a(k),variables b(l)
{
cout<<"init successfully!"<<endl;
}

而另一个,则是会在你每次删除该类对象时运行,长得和构造函数像孪生兄弟一样,只不过在他前面有一个 “ ~ ” 作为区分。同样,他也不会返回任何类型,并且也 不能带有任何参数 。析构函数有助于在跳出程序(如关闭文件、释放内存等)前释放资源。

1
2
3
4
classname::~classname()
{
cout<<"Object is being deleted successfully!"<<endl;
}

可以抽象理解为特殊函数吧(应该)。

补充:关于访问修饰符:

1、private:一般来说,类中的 默认成员属性 就是 private(也就是说如果没有使用任何访问修饰符,类的成员将被假定为私有成员),它代表你不能从外部进行访问,或许可以通过类内部的函数(一般将这个函数设为公共成员)或友元函数调用,但是直接霸王硬上弓你想都别想。

b

2、public:基本与private相对的,从外部可以 直接访问

3、protected:一般来说跟 private 基本没什么不同,但是一旦给当前类派生一个子类,那么其子类就可以 随意调用 父类的 protected 中所有成员,而 private 就算是子类也调用不了(emmmmm,感觉就像给儿子的零花钱和老父亲的私房钱一样……)

2、对象

清醒点找对象了

比如说我们定义了一个 animal 类,那么就可以用他来定义对象(效仿 int num 这种)

1
2
animal bird;	// 声明 animal,类型为 bird
animal monkey; // 声明 animal,类型为 monkey

我们定义出来的对象包含的数据都是以对应类为标准制作的

调用任何权限允许的公共数据,可以使用直接成员访问运算符 “ . ” 来访问。

而私有与受保护的对象就不能够由此运算符调用

3、继承

graph TB
A("————
|Vehicle|
————
drive()
sit in()")-->B("———
|Plane|
———
fly()")

如上图,这里我们设置了飞机和载具两个不同的类,但是飞机和载具是存在一层 包含关系 的,即载具共有的性质,飞机理应全部拥有。那么我们通过 C++ 的 继承 特性就可以将载具包含的方法原封不动的传递给飞机,而在这个关系中,我们把载具类称为 基类,而被包含的飞机则称为 派生类

在程序中,格式如下:

1
class derived-class: Access specifier(访问修饰符) base-class

这里的默认修饰符依然是 private

关于访问权限:

这里可以总结一张表:

public protected private
同类
派生类 X
外部类 X X

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承中的特点

有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

  • 1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
  • 2.protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
  • 3.private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

但无论哪种继承方式,上面两点都没有改变:

  • 1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
  • 2.protected 成员可以被派生类访问。
多继承:

每个派生类一定只有一个父类吗?——当然不一定,采用多继承的方式,可以让一个派生类从多个父类那里继承特性……

d

具体声明方式如下

1
2
3
4
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,……
{
<派生类类体>
};

这里仍然使用的是public,private,protected作为访问修饰符。

简单说一下一个不怎么常用的继承方式:虚拟继承

比如当 B,C 两个类都继承自 A 类时,这时再声明一个类 D 继承自 B 和 C,此时由于 B、C 中都存在从 A 处继承下来的同名变量出现了重复,我们就将 A 声明为虚拟继承。

例:

1
2
3
4
class A{......};
class B: virtual public A{......};
class C: virtual public A{......};//虚拟继承
class D: public B, public C{.....};

不过说实在的,尽量少用

e

二、重载:

1、函数重载

简单来说,就是重复定义了同一个名字的函数,我们可以通过修改传入参数的定义或者函数内部的定义来实现对不同数据的分类处理。

例:

1
2
3
4
5
6
7
8
9
10
11
12
int f(int a,int b) {
return a + b;
}

double f(double a, double b) {
return a + b;
}
int main()
{
cout<<f(5,7)<<endl;
cout<<f(5.67,7.8)<<endl;
}

这两个都是在描述同一个函数,main函数中调用时会根据选取的参数类型自动挑选合适的函数部分,最后会得到:

1
2
12
13.47

需要注意的是重载函数不要 仅以返回类型 或者 仅以形参名字 作为区分,编译器是分不清的,他只会报错。

重载函数还可以设定一些默认值,方便输入信息不全时补全

例:

1
2
3
4
5
6
7
int function(int x,int y=7,int z=9){
return x+y+z;
}//(正确)⭕

int function(int x=7,int y=9,int z){
return x+y+z;
}//(错误)❌

这里需要注意的是在声明传入参数时,先将所有没有默认值的形参写入,再考虑有默认值的形参,不然你设定默认值就毫无意义了。

2、运算符重载

不光函数可以重新定义或重载,这(绝大多数内置)运算符也做得到啊!!!

当在重载运算符时,运算符该有多少传入的参数就定义多少,比如定义一个类的加法:

1
2
3
4
5
6
7
8
9
10
11
12
class Square{
public:
double length;
double breadth;
Square operator+(const Square& s){
Square new_square;
new_square.length=this->length+s.length;
new_square.breadth=this->breadth+s.breadth;
return new_square;
}
private:
}//不太规范(没有构造和析构函数什么的),懂意思就行

然后直接用两个square1+square2就可以直接进行相加。当然由于this指针的缘故只用传入其中一个参数。

关于特殊运算符的重载:

实际上之后说到迭代器的时候还要用到,比如说++与–运算自加自减符都会随着其前置或者后置而改变自身定义,我们对这两个运算符的重载也稍有不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//假设我们还是取上面编写好的 SQUARE 类
class Square{
public:
double length;
double breadth;
Square operator++ (){
length++;
breadth++;
return *this;
}//前缀
Square operator++ (int){//括号中插入了 int 来表示后缀
Square s;
s.length=length++;
s.breadth=breadth++;
return s;
}
private:
}

其它运算符的重载方式基本大同小异,详细了解可参考 这个网站

三、简单迭代器的实现:

1、关于vector容器

对每种容器来说,C++ 里其实都有配套的迭代器,只不过老师这边相当离谱的把这个 ban 了叫你自己造一个,那首先还是了解一下 C++ 自带的容器好方便我们依葫芦画瓢~

可以将 vector 简单理解为一个能存放任意数据类型的动态数组,容器使用一个内存分配器对象来动态地处理它的存储需求。vector 一般按照 严格的线性顺序排列,可以通过元素在序列中的位置访问对应的元素。而动态数组这一属性则赋予了它 快速访问,对元素快捷的增删改查能力

1.构造函数

  • vector():创建一个空vector
  • vector(int nSize):创建一个vector,元素个数为nSize
  • vector(int nSize,const t& t):创建一个vector,元素个数为nSize,且值均为t
  • vector(const vector&):复制构造函数
  • vector(begin,end):复制[begin,end)区间内另一个数组的元素到vector中

2.增加函数

  • void push_back(const T& x):向量尾部增加一个元素X
  • iterator insert(iterator it,const T& x):向量中迭代器指向元素前增加一个元素x
  • iterator insert(iterator it,int n,const T& x):向量中迭代器指向元素前增加n个相同的元素x
  • iterator insert(iterator it,const_iterator first,const_iterator last):向量中迭代器指向元素前插入另一个相同类型向量的[first,last)间的数据

3.删除函数

  • iterator erase(iterator it):删除向量中迭代器指向元素
  • iterator erase(iterator first,iterator last):删除向量中[first,last)中元素
  • void pop_back():删除向量中最后一个元素
  • void clear():清空向量中所有元素

4.遍历函数

  • reference at(int pos):返回pos位置元素的引用
  • reference front():返回首元素的引用
  • reference back():返回尾元素的引用
  • iterator begin():返回向量头指针,指向第一个元素
  • iterator end():返回向量尾指针,指向向量最后一个元素的下一个位置
  • reverse_iterator rbegin():反向迭代器,指向最后一个元素
  • reverse_iterator rend():反向迭代器,指向第一个元素之前的位置

5.判断函数

  • bool empty() const:判断向量是否为空,若为空,则向量中无元素

6.大小函数

  • int size() const:返回向量中元素的个数
  • int capacity() const:返回当前向量所能容纳的最大元素值
  • int max_size() const:返回最大可允许的vector元素数量值

7.其他函数

  • void swap(vector&):交换两个同类型向量的数据
  • void assign(int n,const T& x):设置向量中前n个元素的值为x
  • void assign(const_iterator first,const_iterator last):向量中[first,last)中元素设置成当前向量元素

vector 容器包含在 <vector> 中,所以需要 #include <vector> 导入。

常见使用语法很简单:

1
2
3
4
5
6
7
8
9
10
vector<int/double/char/...> someone1; //这样我们就定义了一个容器
//当然,我们也可以对这个容器做一些更具体的设置,比如:
vector<int> someone2(5); //初始化了5个默认值为0的元素
vector<int> someone3(5,1); //初始化了5个值为1的元素
vector<int> someone4{1,2,3};
vector< vector<int> > someone5{{1,2,3},{4,5,6}}; //直接赋值(一维/二维)
vector<int> someone6(someone2); //同类型的vector可以互相初始化
int a[5] = {1,2,3,4,5};
//通过数组a的地址初始化,注意地址是从0到5(左闭右开区间)
vector<int> someone7(a, a+5);

2、迭代器工作原理及作用

实际上我们从 vector 容器的定义中就可以知道:它主要依赖于指针的调用来操作元素,因此,我们在编写迭代器的时候就一定要将指针作为核心来考虑。

一般简单的迭代器至少要有 初始化返回自身指针指针算数 这三种操作。初始化可以使用函数重载,如:

1
2
3
4
5
6
7
8
9
struct List_iterator {
Node* data;
List_iterator() {
data = nullptr;
}//不附带传入参数的初始化默认为空指针
List_iterator(People* other) {
data = other;
}
}//这样实现了附带传入参数的初始化

而返回自身指针、指针算数这类属于带有运算符的特殊操作,则需要用到运算符重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List_iterator& operator ++ () { 
data = data->next;
return *this;
}
List_iterator& operator ++ (int) {
List_iterator* pre=new List_iterator;
pre->data= this->data;
data = data->next;
return *pre;
}
People* operator * () {
return data;
}
bool operator != (const List_iterator& rhs) {
return (rhs.data != data);
}
bool operator == (const List_iterator& rhs) {
return (rhs.data == data);
}

主要注意自加自减的前后缀区分。

a

四、泛型编程初版——模板:

1、函数模板

模板是什么?

想象一下,如果只让我们使用函数重载,编写实现对应不同数据类型不同参数量的函数,会怎么样呢?或许一两个简单函数对你来处理绰绰有余,但要是上百个、上千个呢?你妥妥的会累死……

针对这种情况,泛型编程 的思想应运而生——我们不需要确切编写针对某一种数据类型的函数或行为,我们只需要给出执行方式,让编译器自己去对应正确类型,省时省力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename type> (返回类型) (函数名)(参数表)
{
// 函数的主体
}
//例:
template <typename key, typename target, typename peo, typename list>
int statistic(key info, target ch, peo l,list f) {
int num = 0;
People* pre = l->next;
while (pre)
{
if (info.compare(pre, ch)) {
num++;
f.copy_(pre);
}
pre = pre->next;
}
f.display();
return num;
}

:这里的 type 是占位符类型名称,可以在类被实例化的时候进行指定。程序员可以使用一个逗号分隔的列表来定义多个泛型数据类型。

2、类模板

其实不止于对函数我们可以使用模板来“偷懒”,类同样可以通过模板来实现更高效的运行与操作。最典型的案例当属链表这种可以存储多种类型数据的结构体。

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
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
return elems.back();
}


//而当我们调用这个模板类时,只需要标明数据类型即可:
Stack<int/double/char/...> new_stack;

在外围进行成员函数的定义时注意遵守 template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体};的语法,模板形参列表一定要和最初设定时 完全一致 才行。

在这里请注意,类模板是不存在 实参推演 的,也就是说直接输入 Stack<2> new_stack; 会引起错误。

五、Makeflie编写:

1、准备工作

讲道理一开始小实验做到最后发现要写 Makefile 我是想骂人的,不是说这个多难学,而是大一就装了虚拟机或者使用 Linux 系统的人真的不多,况且这个老师我不信他用个 IDE 能要了他老人家的命……

f

具体步骤:

  • Linux 下随便找个文件夹当受害者,把你的cpp文件塞进去,sln的不要。
  • 建一个名字为 Makefile 的文本文件(这里新版本可以识别首字母小写的“makefile”)
  • 打开它,开始进行编写(别用 vim 刨)

2、编写格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
例2:
myprogram: myprogram.cpp support.o
g++ myprogram.cpp support.o -o myprogram
support.o: support.h support.cpp
g++ support.h support.cpp -o support.o
clean:
rm *.o myprogram
——————————————————————————————————————————————
例1:
CPP: CPPprogram.cpp
g++ CPPprogram.cpp -o CPP

status.o: templates.h list.h function.h People.h status.h
g++ -c templates.h list.h function.h People.h status.h CPPprogram.cpp

clean:
rm CPP

这里要注意几个格式规范:

  1. 冒号后面有一个空格符
  2. g++ 前面是一个 “tab” 键的距离

除了 -o 合并为可操作文件,还可以用 -c 简单组合。makefile 的编写遵守 从后往前 的规则,最先写的是最后合并的,编译从下往上进行。

  • 用了 -o 就千万不能缺少 main 函数,毕竟是执行文件;

所以如果你只是想把几段代码拼起来,那么建议还是用 -c

  • 这里的 clean 其实并不存在对应的文件,被称为 伪目标。为了避免和文件重名的这种情况,我们可以使用一个特殊的标记 “ .PHONY ” 来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”。

rm 这个玩意用来删除文件或者目录 -i 表示删除前逐一询问确认; -f 表示即使原档案属性设为只读,亦直接删除,无需逐一确认; -r 表示将目录及以下之档案亦逐一删除。例:

1
rm -r [filename]

g