C++运算符重载(简单易懂)

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

你可以重定义或重载大部分 C++ 内置的运算符。例如 + 、 - 、 * 、 / 、

++、–、>>、<<等,这样,你就能使用自定义类型的运算符。

运算符重载的基本格式

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和

其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个

返回类型和一个参数列表。

Point operator+(const Point &);

运算符重载有两种方式:一种是类内重载(运算符重载函数作为类的成员函数),另一种是类外重载(运算符重载函数作为类的友元函数)

类内重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Point{
public:
Point(){};
Point (int x, int y): x(x),y(y) {};
Point operator+(const Point &a){ //类内重载,运算符重载函数作为类的成员函数
Point ret;
ret.x = this->x + a.x;
ret.y = this->y + a.y;
return ret;
}
int x,y;
};

int main() {
Point a(2,4),b(5,3);
Point c = a + b;
cout<< "x :" << c.x << endl;
cout<<"y :" << c.y << endl;
}

当上面的代码被编译和执行时,它会产生下列结果:

x : 7

y: 7

运算符重载是类内重载时,运算符重载函数作为类的成员函数,以上述代码为例 a + b 相当于 a 对象调用+方法并且传入参数时 b 对象

类外重载

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;

class Point{
public:
Point(){};
Point (int x, int y): x(x),y(y) {};
friend Point operator+(const Point &, const Point &);
int x,y;
};

Point operator+(const Point &a,const Point &b){//类外重载,运算符重载函数作为类的友元函数
Point ret;
ret.x = a.x + b.x;
ret.y = a.y + b.y;
return ret;
}

int main() {
Point a(2,4),b(5,3);
Point c = a + b;
cout<< "x :" << c.x << endl;
cout<<"y :" << c.y << endl;
}

当上面的代码被编译和执行时,它会产生和上面一样的结果

各种运算符重载实例

下面将进行各种运算符重载实例的代码演示,演示几种基本的运算符重载。

插入运算符重载>> and 提取运算符重载<<

以提取运算符重载<<为例,coutostream类的对象。ostream 类和 cout 都是在头文件 <iostream>中声明的。ostream 类将<<重载为成员函数。

下面我们重载<<使用cout输出a对象

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

class Point{
public:
Point(){};
Point (int x, int y): x(x),y(y) {};
friend Point operator+(const Point &, const Point &);
friend ostream &operator<<(ostream &out , const Point &a);
private:
int x,y;
};

Point operator+(const Point &a,const Point &b){
Point ret;
ret.x = a.x + b.x;
ret.y = a.y + b.y;
return ret;
}

ostream &operator<<(ostream &out , const Point &a){
out << "<Point>( " << a.x << ", " << a.y << ")";
return out;
}

int main() {
Point a(2,4),b(5,3);
Point c = a + b;
cout << c<< endl;
}

当上面的代码被编译和执行时,它会产生下列结果:

< Point>( 7, 7)

注意:重载<<时,是类外重载,习惯上人们是使用cin>>cout<< 的,得使用友元函数来重载运算符,如果使用成员函数来重载会出现 c<<cout; 这种不自然的代码。

另外应该会有人对ostream &operator<<(ostream &out , const Point &a)函数感到疑惑,首先在重载<<时,返回值类型是ostream&, 第一个参数也是ostream&。也就是说,表达式cout<<c的返回值仍是 cout,所以cout<<c<<endl;才能成立

前置运算符重载++ and 后置运算符重载++

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

class Point{
public:
Point(){};
Point (int x, int y): x(x),y(y) {};
friend Point operator+(const Point &, const Point &);
friend ostream &operator<<(ostream &out , const Point &a);
Point& operator++(){ //前置运算符,需要引用返回,不需要参数。返回自增后的值,且返回的是一个左值
x++;
y++;
return *this;
}

const Point operator++(int){//后置++,不需要引用返回,需要参数区分。返回自增前的值,且返回的是一个右值
Point temp(x,y);
x++;
y++;
return temp;
}
private:
int x,y;
};

Point operator+(const Point &a,const Point &b){
Point ret;
ret.x = a.x + b.x;
ret.y = a.y + b.y;
return ret;
}

ostream &operator<<(ostream &out , const Point &a){
out << "<Point>(" << a.x << " , " << a.y << ")";
return out;
}


int main() {
Point a(2,4),b(5,3);
Point c = a + b;
cout << c << endl;
c++;
cout << c << endl;
++c;
cout << c << endl;
}

当上面的代码被编译和执行时,它会产生下列结果:

(7 , 7)
< Point>(8 , 8)
< Point>(9 , 9)

1>为区别前置和后置运算符,需要在后置运算符重载函数中加参数“int”,虽然这个类型在此除了以示区别之外并不代表任何实际含义;

2>前置返回的是变量的引用,后置返回的是常量。所以++++c合法,而c++++不合法;

3>为什么不让c++++也合法呢?如果要实现c++++合法,必须使后置返回变量或变量的引用。c++是先返回c值再+1,所以不可能返回c,那就只能先建立局部变量来保存c的初值,然后再返回局部变量(局部变量不允许返回引用),但返回了局部变量之后,如果再连着进行下一次++运算,参与运算的就是这个局部变量的值了,所以此时c++++其实等效与c++,也就没有存在的意义了。

C++中new/delete 和malloc/free的区别

1、new、delete是C++中的操作符,而malloc和free是标准库函数。

2、对于非内部数据对象来说,只使用malloc是无法完成动态对象要求的,一般在创建对象时需要调用构造函数,对象消亡时,自动的调用析构函数。而malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而NEW在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理,delete也可以自动调用析构函数。而mallloc只是做一件事,只是为变量分配了内存,同理,free也只是释放变量的内存。

3、new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而 malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。

new 和 delete 到底是什么?

如果找工作的同学看一些面试的书,我相信都会遇到这样的题:sizeof 不是函数,然后举出一堆的理由来证明 sizeof 不是函数。在这里,和 sizeof 类似,new 和 delete 也不是函数,它们都是 C++ 定义的关键字,通过特定的语法可以组成表达式。和 sizeof 不同的是,sizeof 在编译时候就可以确定其返回值,new 和 delete 背后的机制则比较复杂。
继续往下之前,请你想想你认为 new 应该要做些什么?也许你第一反应是,new 不就和 C 语言中的 malloc 函数一样嘛,就用来动态申请空间的。你答对了一半,看看下面语句:

1
string *ps = new string("hello world");

你就可以看出 new 和 malloc 还是有点不同的,malloc 申请完空间之后不会对内存进行必要的初始化,而 new 可以。所以 new expression 背后要做的事情不是你想象的那么简单。在我用实例来解释 new 背后的机制之前,你需要知道 operator newoperator delete 是什么玩意。

operator new 和 operator delete

这两个其实是 C++ 语言标准库的库函数,原型分别如下:

1
2
3
4
5
void *operator new(size_t);     //allocate an object
void *operator delete(void *); //free an object

void *operator new[](size_t); //allocate an array
void *operator delete[](void *); //free an array

后面两个你可以先不看,后面再介绍。前面两个均是 C++ 标准库函数,你可能会觉得这是函数吗?请不要怀疑,这就是函数!C++ Primer 一书上说这不是重载 new 和 delete 表达式(如 operator= 就是重载 = 操作符),因为 new 和 delete 是不允许重载的。但我还没搞清楚为什么要用 operator new 和 operator delete 来命名,比较费解。我们只要知道它们的意思就可以了,这两个函数和 C 语言中的 malloc 和 free 函数有点像了,都是用来申请和释放内存的,并且 operator new 申请内存之后不对内存进行初始化,直接返回申请内存的指针。

我们可以直接在我们的程序中使用这几个函数。

new 和 delete 背后机制

知道上面两个函数之后,我们用一个实例来解释 new 和 delete 背后的机制:

我们不用简单的 C++ 内置类型来举例,使用复杂一点的类类型,定义一个类 A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
public:
A(int v) : var(v)
{
fopen_s(&file, "test", "r");
}
~A()
{
fclose(file);
}

private:
int var;
FILE *file;
};

很简单,类 A 中有两个私有成员,有一个构造函数和一个析构函数,构造函数中初始化私有变量 var 以及打开一个文件,析构函数关闭打开的文件。

我们使用

1
class *pA = new A(10);

来创建一个类的对象,返回其指针 pA。如下图所示 new 背后完成的工作:

简单总结一下:

  1. 首先需要调用上面提到的 operator new 标准库函数,传入的参数为 class A 的大小,这里为 8 个字节,至于为什么是 8 个字节,你可以看看《深入 C++ 对象模型》一书,这里不做多解释。这样函数返回的是分配内存的起始地址,这里假设是 0x007da290。
  2. 上面分配的内存是未初始化的,也是未类型化的,第二步就在这一块原始的内存上对类对象进行初始化,调用的是相应的构造函数,这里是调用 A:A(10); 这个函数,从图中也可以看到对这块申请的内存进行了初始化,var=10, file 指向打开的文件
  3. 最后一步就是返回新分配并构造好的对象的指针,这里 pA 就指向 0x007da290 这块内存,pA 的类型为类 A 对象的指针。

所有这三步,你都可以通过反汇编找到相应的汇编代码,在这里我就不列出了。

好了,那么 delete 都干了什么呢?还是接着上面的例子,如果这时想释放掉申请的类的对象怎么办?当然我们可以使用下面的语句来完成:

1
delete pA;

delete 所做的事情如下图所示:

img

delete 就做了两件事情:

  1. 调用 pA 指向对象的析构函数,对打开的文件进行关闭。
  2. 通过上面提到的标准库函数 operator delete 来释放该对象的内存,传入函数的参数为 pA 的值,也就是 0x007d290。

好了,解释完了 new 和 delete 背后所做的事情了,是不是觉得也很简单?不就多了一个构造函数和析构函数的调用嘛。

如何申请和释放一个数组?

我们经常要用到动态分配一个数组,也许是这样的:

1
2
string *psa = new string[10];      //array of 10 empty strings
int *pia = new int[10]; //array of 10 uninitialized ints

上面在申请一个数组时都用到了 new [] 这个表达式来完成,按照我们上面讲到的 new 和 delete 知识,第一个数组是 string 类型,分配了保存对象的内存空间之后,将调用 string 类型的默认构造函数依次初始化数组中每个元素;第二个是申请具有内置类型的数组,分配了存储 10 个 int 对象的内存空间,但并没有初始化。

如果我们想释放空间了,可以用下面两条语句:

1
2
delete [] psa;
delete [] pia;

都用到 delete [] 表达式,注意这地方的 [] 一般情况下不能漏掉!我们也可以想象这两个语句分别干了什么:第一个对 10 个 string 对象分别调用析构函数,然后再释放掉为对象分配的所有内存空间;第二个因为是内置类型不存在析构函数,直接释放为 10 个 int 型分配的所有内存空间。

这里对于第一种情况就有一个问题了:我们如何知道 psa 指向对象的数组的大小?怎么知道调用几次析构函数?

这个问题直接导致我们需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

还是用图来说明比较清楚,我们定义了一个类 A,但不具体描述类的内容,这个类中有显示的构造函数、析构函数等。那么 当我们调用

1
class A *pAa = new A[3];

时需要做的事情如下:

img

从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小,但是最终返回的是对象数组的指针,而不是所有分配空间的起始地址。

这样的话,释放就很简单了:

1
delete [] pAa;

img

这里要注意的两点是:

  • 调用析构函数的次数是从数组对象指针前面的 4 个字节中取出;
  • 传入 operator delete[] 函数的参数不是数组对象的指针 pAa,而是 pAa 的值减 4。

为什么 new/delete 、new []/delete[] 要配对使用?

其实说了这么多,还没到我写这篇文章的最原始意图。从上面解释的你应该懂了 new/delete、new[]/delete[] 的工作原理了,因为它们之间有差别,所以需要配对使用。但偏偏问题不是这么简单,这也是我遇到的问题,如下这段代码:

1
2
int *pia = new int[10];
delete []pia;

这肯定是没问题的,但如果把 delete []pia; 换成 delete pia; 的话,会出问题吗?

这就涉及到上面一节没提到的问题了。上面我提到了在 new [] 时多分配 4 个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(如内置类型,这里的 int 数组)?我们在 new [] 时就没必要多分配那 4 个字节, delete [] 时直接到第二步释放为 int 数组分配的空间。如果这里使用 delete pia;那么将会调用 operator delete 函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间。不存在问题的。

这里说的使用 new [] 用 delete 来释放对象的提前是:对象的类型是内置类型或者是无自定义的析构函数的类类型!

我们看看如果是带有自定义析构函数的类类型,用 new [] 来创建类对象数组,而用 delete 来释放会发生什么?用上面的例子来说明:

1
2
class A *pAa = new class A[3];
delete pAa;

那么 delete pAa; 做了两件事:

  • 调用一次 pAa 指向的对象的析构函数;
  • 调用 operator delete(pAa); 释放内存。

显然,这里只对数组的第一个类对象调用了析构函数,后面的两个对象均没调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。

上面的问题你如果说没关系的话,那么第二点就是致命的了!直接释放 pAa 指向的内存空间,这个总是会造成严重的段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地方减去 4 个字节的地方。你应该传入参数设为那个地址!

同理,你可以分析如果使用 new 来分配,用 delete [] 来释放会出现什么问题?是不是总会导致程序错误?

总的来说,记住一点即可:new/delete、new[]/delete[] 要配套使用总是没错的!

关于iostream与using namespace std 的解析

当我们在使用C++语法进行编程练习的时候,往往在代码的前两行会这样来写

1
2
include<iostream>
using namespace std;

学过C语言的同学在学习C++的时候是否会产生这样的疑惑呢:

练习C语言编程的时候,只需要声明一个头文件:#include<stdio.h>.而为什么在C++中,却需要写出include和using namespace std;这两行代码呢?

其中第一行include我们还勉强可以理解,它其实类似于C语言中的#include<stdio.h>,即:声明标准的输入输出头文件。然而using namespace std究竟起到了什么作用呢?

针对这个问题,网络上有很多专业的说法,但是长篇大论的内容,对于初学者来说实在头疼,根本看不进去,所以接下来我希望可以用简练的语言来解释清楚using namespace std的作用,以下的内容虽可能有些许冗余,但我相信是通俗易懂的,您只要能够一行行的看下去,一定能够完全看懂!

本人认为正确的学习顺序是:我们要做到先学会定义、使用和理解命名空间(namespace),再回过头去研究using namespace std;的作用,这样可以起到事半功倍的效果,那么接下来重点就来了。

定义命名空间

(1)简单的命名空间

1
2
3
4
5
//1、定义命名空间A
namespace A
{
int a=0;//在命名空间A中定义变量a
}

定义一个命名空间就是这样简单:在这个命名空间A中,有一个int型的变量,他的名字是“a”。那么我们接下来直接上难度!定义一个复杂的命名空间。

(2)复杂的命名空间

要求:定义命名空间B,并在命名空间B中定义命名空间C,并在命名空间C中定义结构体Teacher

1
2
3
4
5
6
7
8
9
10
11
12
13
//2、复杂的命名空间
namespace B //定义命名空间B
{
int a=1;//在命名空间B中也可以定义变量a,因为他们不在同一个命名空间中
namespace C//还可以在命名空间B中再定义一个命名空间C
{
struct Teacher//在命名空间C中定义结构体Teacher
{
char a[32];//结构体中还可以定义变量a,因为这三个“a”都不在同一个命名空间中
int b;
};
}
}

使用命名空间

(1)使用命名空间的基本方法

前面已经定义好了命名空间A、B、C,且在这三个命名空间中都定义相同的变量“a”,接下来我们就来使用这三个命名空间。

1
2
3
4
5
6
int main()
{
//使用命名空间的基本方法1
using namespace A;//告诉编译器我要使用命名空间A中的功能了
a=10; //将命名空间A中的变量“a”由0更改为10
cout<<a<<endl; //查看是否完成了更改,此时屏幕上应该输出“10

注:此时只使用了命名空间A,所以只会对命名空间A中的变量a进行访问,而不会对命名空间B、C中的变量a进行访问。

而实际上,针对上面这三行代码,只要第三行代码中变量“a”的指向明确,便可以省去第一行“using namespace A”,如下:

1
2
3
4
//使用命名空间的基本方法2
//由于在代码中只要使用这两种方法中的其中一种即可,所以我在第二种方法这里加上的“//”注释符号,如下两行代码:
// a=10;
// cout<<A::a<<endl; //“A::a”明确指向了命名空间A中的变量a(用双冒号::)

以上两种使用命名空间的方法供君随意挑选^_^

(2)使用复杂的命名空间

命名空间A的使用相对简单,但是在命名空间B中嵌套着命名空间C,并且命名空间C中又嵌套着结构体Teacher,那么我们又该如何去使用命名空间B和命名空间C呢?

接下来,除了使用命名空间A以外,如果我们还使用了命名空间B的话,” a=10; “ 这条语句的指向就不明确了:因为命名空间B中也有个变量a,于是现在有两个”a”可以被访问。故:若想访问某一个特定的变量a就需要指向明确!

1
2
3
4
using namespace B;
//如果想访问某一个特定的变量a,就需要指向明确!如下:
cout<<A::a<<endl; //明确指向了“命名空间A”
//虽然现在有A、B两个命名空间,且两个命名空间中都有变量“a”,但只要指向明确,就能指定我们所访问的变量究竟是命名空间A中的“a”还是命名空间B中的“a”

(3)使用结构体Teacher中的变量

1
2
3
4
5
6
7
8
9
10
//如何使用Teacher
//方法1:这种方法每定义一个结构体就需要:“B::C::Teacher 结构体名称”
//B::C::Teacher t1;//用命名空间B中的命名空间C中的结构体模板(Teacher)来定义结构体t1
//t1.b=33;//注意:t1.a 语法错误!是因为a是一个指针(地址),这个指针是无法被修改的,否则以后无法释放内存
//方法2:先一口气直接定义好“结构体型”数据类型,然后按照根据数据类型定义变量的方法来使用结构体
using B::C::Teacher;
Teacher t2;
t2.b=22;
return 0;
}

关于iostream与using namespace std 的解析****

(1)通过以上关于命名空间的定义及使用的介绍,我们不难发现:不同的命名空间之间是相互独立的个体,虽然附加在其中的变量名可能是相同的(比如上面所提到的命名空间A、B、C中都包含有变量a),但是没关系:“命名空间” 这层 “屏障”将这些相同的变量名分隔开来,让他们虽然拥有相同的名字,但是互不影响。

(2)看起来命名空间的引入十分方便,让我们不必再因为变量重名而烦恼。但是在以前,并没有命名空间这个概念,而是将标准库功能定义在全局空间里,并声明在<iostream.h>(早期的C++头文件)中。但是由于标准库非常的庞大,那么程序员在选择的类的名称或函数名时,就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都放在一个名为std的命名空间中。后来C++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h,即出现了现在的头文件。

(3)当使用的时候,该头文件没有定义全局命名空间,必须使用C++所规定的标准的命名空间(即:namespace std),这样才能正确使用cout、endl等功能。

(4)最后:如果大家想进一步了解相关内容,请参考:点击打开链接 谢谢。

c++定义结构体

先定义结构体类型再单独进行变量定义

1
2
3
4
5
6
7
8
9
10
struct Student
{
int Code;
char Name[20];
char Sex;
int Age;
};
struct Student Stu;
struct Student Stu[10];
struct Student *pStru;

结构体类型是struct Student,因此,struct和Student都不能省略。但实际上,我用codeblocks运行时,下面变量的定义,不加struct 也是可以的。

紧跟在结构体类型说明之后进行定义

1
2
3
4
5
6
7
struct Student
{
int Code;
char Name[20];
char Sex;
int Age;
}Stu,Stu[10],*pStu;

这种情况时,后面还可以再定义结构体变量。

在说明一个无名结构体变量的同时直接进行定义

1
2
3
4
5
6
7
struct
{
int Code;
char Name[20];
char Sex;
int Age;
}Stu,Stu[10],*pStu;

这种情况下,之后不能再定义其他变量。

使用typedef说明一个结构体变量之后再用新类名来定义变量

1
2
3
4
5
6
7
8
typedef struct
{
int Code;
char Name[20];
char Sex;
int Age;
}student;
Student Stu,Stu[10],*pStu;

Student是一个具体的结构体类型,唯一标识。这里不用再加struct

同步与异步

什么是同步与异步

定义:同步和异步关注的是*消息通信机制* (synchronous communication/ asynchronous communication)。同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。异步,和同步相反 调用方不会理解得到结果,而是在调用发出后调用者可用继续执行后续操作,被调用者通过状体来通知调用者,或者通过回掉函数来处理这个调用**

比方说:你去商城买东西,你看上了一款手机,能和店家说你一个这款手机,他就去仓库拿货,你得在店里等着,不能离开,这叫做同步。现在你买手机赶时髦直接去京东下单,下单完成后你就可用做其他时间(追剧、打王者、lol)等货到了去签收就ok了.这就叫异步。

以方法调用为例

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作

加两张图更好理解

同步执行

img

同步执行当调用方法执行完成后并返回结果,才能执行后续代码

异步执行

img

异步调用的话可用参考ajax,调用方法后不会等到sum方法执行完成,而是直接执行后续代码。sum方法执行完成后主要通过状态通知主线程,或者通过回调处理这次异步方法执行的结果

同步异步于阻塞非阻塞不能混为一谈

看了上面的讲解,你可能会说这不就是阻塞机制吗?不不不,同步异步不能和阻塞非阻塞混为一谈。

阻塞和非阻塞 强调的是程序在等待调用结果(消息,返回值)时的状态. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 对于同步调用来说,很多时候当前线程还是激活的状态,只是从逻辑上当前函数没有返回而已,即同步等待时什么都不干,白白占用着资源。

同步和异步强调的是消息通信机制 (synchronous communication/ asynchronous communication)。所谓同步,就是在发出一个”调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。而异步则是相反,”调用”在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在”调用”发出后,”被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用

参考博客:https://blog.csdn.net/huangqiang1363/article/details/79508852

c语言分区

  1. 栈Stack(大地址,由高向低使用)
  2. 堆Heap(由低向高使用)
  3. 全局or静态常量区
  4. 文本and代码区(小地址)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdlib.h>
#include<stdio.h>

int total = 0; //全局or静态常量区

void hehe() { //函数,在stack中
static int he = 0; //全局or静态常量区,因为static只会创建一次
he++;
total++;
}

int main() {
int k = 3; //函数的内部变量,在stack中,但3在代码区
char *str = "Hello, world!"; // str函数中内部变量,在stack中,“hello world”在常量区
int *p = (int *) malloc(sizeof(int)); // p在stack中,malloc动态分配的内存在堆中
hehe();
hehe();
printf("%d%s%d", k, str, *p);
free(p);
return 0; //stack中内存释放
}

类和对象

  • :类是一个模板,它描述一类对象的行为和状态。
  • 对象:对象是类的一个实例

c语言如何处理类和对象:

  • 没有“类”,只有“类型”

  • 没有“对象”,只有“变量”

  • 结构体变量+函数

    c语言采用结构体变量+函数的方式来实现类似功能,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include "stdio.h"

    struct Student{
    int id;
    };

    void printID(struct Student *in){
    printf("My id is %d.\n",in->id);
    }

    int main(){
    struct Student one;
    one.id=999;
    printID(&one);
    return 0;
    }

但我们发现,Student的属性和加在属性上的操作是割裂开的,我们可以使用函数指针类解决此问题

  • 函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stdio.h"

struct Student{
int id;
void (*printID)(struct Student *in);
};

void printID(struct Student *in){
printf("My id is %d.\n",in->id);
}

int main(){
struct Student one;
one.id=999;
one.printID=printID;
one.printID(&one);
return 0;
}

不一样的体验,用纯文本命令行写C++程序

  1. 打开vmware运行ubuntu
  2. 桌面右键打开终端
  3. 输入命令ls查看当前所在位置
  4. 输入命令cd 桌面进入桌面这个文件夹
  5. 输入命令touch test.cpp创建test.cpp文件
  6. 打开test.cpp文件,输入以下代码并保存在这里插入图片描述
  7. 输入命令g++ test.cpp编译test.cpp文件,我们发现桌面上产生了一个新的文件a.out,这就是编译产生的可执行文件,接下来我们运行之
  8. 输入命令./a.out运行,发现如下输出:在这里插入图片描述

c++的新特性

类(class)

之前说过了

bool类型和auto类型

bool,c语言中,真假用整形来代替,0 -->False && !0-->True && True--> && False-->0

c++中,可以true,false,也可以用0和1

auto,让编译器推断是什么类型,但必须在初始化时赋值,否则编译报错

cout,在打印浮点数是会省略后面的部分

引用

1
2
3
4
5
6
7
int a = 3;  //定义了变量a,并用3使其初始化,=不是运算符=含义是初始化
int b; //定义了变量b,没有初始化
b = 3; //把3赋值给b,=是运算符

int array[10]={1,2,3}; //定义了数组array,并用{1,2,3}使其初始化
array={1,2,3}; //错误
array[10]={1,2,3}; //错误
1
2
3
4
int *p; // *的含义是p是一个指针,不是取值运算符,p是指向int类型的指针
int a = 3;
p = &a;
*p = 6; // *的是取值运算符
1
2
3
4
5
int b = 6;
int &r = b; // &的含义是r是一个引用类型,不是取地址运算符,r是int类型的引用,=表示用b来初始化r,让r成为b的引用,不是赋值,可以理解为给b起个外号,自此之后r就是b,b就是r
r = 123;
cout << r << endl; // 123
cout << b << endl; // 123

在C++中函数的参数传递:

  • 按值传递(pass by value)
  • 地址传递(pass by pointer)
  • 引用传递(pass by reference)

引用必须在定义时初始化,一旦创建就不可更换引用的对象

1
2
3
int b = 6;
int &r; //error: 'r' declared as reference but not initialized
r = b; //把b的值赋值给r,但此时r并没有任何的引用

关于赋值

1
2
3
4
5
int a, b, c;
(a = b = c = 3) = 666;
cout << a << endl; //666
cout << b << endl; //3
cout << c << endl; //3

赋值之后返回的是左值的引用

初始化的新语法

1
2
int a{3};
int array[5]{1, 2, 3, 4, 5};

new和delete

1
2
3
4
5
6
7

int *p;
p = (int *) malloc(sizeof(int));
free(p);
p = new int;
delete p;
deleete[] p; //复杂类型应该这样写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "iostream"
using namespace std;
class Student {
public:
int sid;
};
int main() {
Student one, two;
one.sid = 1;
two.sid = 2;
cout << &one << endl;
cout << &two << endl;
one = two;
two.sid = 999;
cout << &one << endl;
cout << &two << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "iostream"
using namespace std;
class Student {
public:
int sid;
};
int main() {
Student *one = new Student();
Student *two = new Student();
// Student *one, *two;
// one = new Student();
// two = new Student();
one->sid = 1;
two->sid = 2;
cout << &one << endl; //0x63fde8
cout << &two << endl; //0x63fde0
one = two; //内存泄漏
two->sid = 999;
cout << one << endl; //0x1c1770
cout << two << endl; //0x1c1770
return 0;
}

进一步理解Java的引用,相当于c++中的指针,java的引用和c++的引用是不一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student {
public int sid;
}

public class test {
public static void main(String args[]) {
Student one = new Student(); //创建对象一定需要new
Student two = new Student();
one.sid = 1;
two.sid = 2;
System.out.println("one: " + one); //one: Student@7291c18f
System.out.println("two: " + two); //two: Student@34a245ab
one = two;
two.sid = 999;
System.out.println("one: " + one); //one: Student@34a245ab
System.out.println("two: " + two); //two: Student@34a245ab
}
}

新的for循环

for-each不再赘述,可以使用auto,each是取出来的容器里的值,&each可以修改容器里本来的值

在这里插入图片描述
在这里插入图片描述

重载

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

在同一个作用域内,可以声明几个同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。

我们可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

注:在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值;

lambda

1
2
auto f = [](auto a, auto b) -> auto { return a + b; };
cout << f(3.1, 5) << endl; // 8.1

泛型和模板

泛型是概念, 模板是泛型的实现
泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。所谓泛型(Genericity),是指具有在多种数据类型上皆可操作的含意,与模板有些相似。STL巨大,而且可以扩充,它包含很多计算机基本算法和数据结构,而且将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起。STL以迭代器(Iterators)和容器(Containers)为基础,是一种泛型算法(Generic Algorithms)库,容器的存在使这些算法有东西可以操作。STL包含各种泛型算法(algorithms)、泛型指针(iterators)、泛型容器(containers)以及函数对象(function objects)。STL并非只是一些有用组件的集合,它是描述软件组件抽象需求条件的一个正规而有条理的架构。

C语言*运算符和&运算符

取址运算符&用来取得其操作数的地址。如果操作数 x 的类型为 T,则表达式 &x 的类型是 T 类型指针(指向 T 的指针)。

取址运算符的操作数必须是在内存中可寻址到的地址。换句话说,该运算符只能用于函数或对象(例如左值),而不可以用于位字段,以及那些还未被存储类修饰符 register 声明的内容。

当需要初始化指针,以指向某些对象或函数时,需要获得这些对象或函数的地址:

1
float x, *ptr;ptr = &x;           // 合法:使得指针ptr指向xptr = &(x+1);       // 错误: (x+1) 不是一个左值

相反地,当已具有一个指针,并且希望获取它所引用的对象时,使用间接运算符 *(indirection operator),有时候这会被称为解引用运算符(dereferencing operator)。它的操作数必须是指针类型。如果 ptr 是指针,那么 *ptr 就是 ptr 所指向的对象或函数。如果 ptr 是一个对象指针,那么 *ptr 就是一个左值,可以把它(即 *ptr)当作赋值运算符左边的操作数:

1
float x, *ptr = &x;*ptr = 1.7;                                      // 将1.7赋值给变量x++(*ptr);                                        // 并将变量x的值加1

在这个示例最后的语句中,ptr 的值保持不变,但 x 的值变成 2.7。

如果指针操作数的值不是某个对象或函数的地址,则间接运算符*的操作结果无法确定。

像其他一元操作数一样,运算符 & 和 * 具有很高的优先级。操作数的组合方式是从右到左。因此,表达式 ++(*ptr)中的括号是没有必要的。

运算符 & 和 * 是互补的:如果 x 是一个表达式,用于指定一个对象或一个函数,那么表达式 &x 就等于 x。相反地,在形如 &ptr 的表达式中,这些运算符会互相抵消,表达式的类型与值等效于 ptr。然而,不管 ptr 是不是左值,&*ptr 都一定不会是左值。

理解封装

类里有什么

  • 数据(属性)
  • 操作(函数、方法)
  • 访问控制(公有/私有)
  • 静态/非静态

直接使用类:静态
使用对象:静态和非静态
静态不能访问非静态

使用类和对象

定义类和对象

代码复用

代码复用的手段

  • 函数重载
  • 类的继承,动态
  • 借口

题外话

构造函数使用初始化列表来初始化字段

1
2
3
4
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}

上面的语法等同于如下语法:

1
2
3
4
5
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}

在不同的字段使用逗号进行分隔,如下所示:

1
2
3
4
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}

模板(Template)

函数模板

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
//
// Created by Zhenglin Li on 2020/9/23.
//

#include "iostream"

using namespace std;

template<typename T>
T sum(T array[], int size) {
T s = 0;
for (int i = 0; i < size; ++i) {
s += array[i];
}
return s;
}

int main() {
int a1[5] = {1, 2, 3, 4, 5};
double a2[3] = {1.1, 2.2, 3.3};
cout << sum(a1, 5) << endl;
cout << sum(a2, 3) << endl;
cout << sum(a2, 1, 3) << endl;
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
26
27
28
29
30
31
32
33
34
//
// Created by Zhenglin Li on 2020/9/23.
//

#include "iostream"

using namespace std;

template<typename T>
T sum(T array[], int size) {
T s = 0;
for (int i = 0; i < size; ++i) {
s += array[i];
}
return s;
}

template<typename T>
T sum(T array[], int from, int to) {
T s = 0;
for (int i = from - 1; i <= to - 1; ++i) {
s += array[i];
}
return s;
}

int main() {
int a1[5] = {1, 2, 3, 4, 5};
double a2[3] = {1.1, 2.2, 3.3};
cout << sum(a1, 5) << endl;
cout << sum(a2, 3) << endl;
cout << sum(a2, 1, 3) << endl;
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
26
27
28
29
30
31
//
// Created by Zhenglin Li on 2020/9/23.
//

#include "iostream"

using namespace std;
//模板
template<typename T>
class Vector {
private:
T x;
T y;
public:
Vector(T x = 0, T y = 0) : x(x), y(y) {

}

friend ostream &operator<<(ostream &o, const Vector<T> &v) {
o << "<" << v.x << ", " << v.y << ">" << endl;
}
};

int main() {
//真正的类,要<>
Vector<int> x(1,2);
Vector<double> *p = new Vector<double>(1.1,2.2);
cout << x << endl;
cout << *p << endl;
return 0;
}

标准模板库(STL)

  • Standard Template Library
  • 很多容器
  • 很多算法