C++面向对象基础——C++基本用法&运算符重载

C++基础知识

命名空间

namespace,C++ 为我们提供了一个允许我们定义在不同作用域的同名函数。我们可以用如下定义:

1
2
3
4
5
namespace name{
void f1();
void f2();
...
}

通过以上的申明我们可以在 namespace 外调用函数,通过如下方式调用

1
2
name::f1();
name::f2();

C++还有 using 关键字,使用 using 之后,我们不必通过以上方式调用,而是可以直接使用它的函数名调用。比如我如果在调用 f1() 之前用了 using namespace name;

那么我就不需要再用 name::f1() 了,而是可以直接使用 f1() 调用指定函数。

namespace 可以嵌套定义。

函数重载

C++开始已经支持了同名函数的存在,在相同 namespace 下也可以定义同名的函数,只要参数类型不一样即可,它会根据你调用的方式选择合适的重载函数,主要是因为 C++ 有一个名称粉碎机制,它会根据你的参数名和参数个数以及参数类型重新生成新的函数,当然如果你定义两个参数列表和参数名完全一样的函数一样会报错。

类与对象

类:具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象,类实际上就是一种数据类型。类具有属性,它是对象的状态的抽象,用数据结构来描述类的属性。类具有操作,它是对象的行为的抽象,用操作名和实现该操作的方法来描述。

对象:对象是人们要进行研究的任何事物,它不仅能表示具体的事物,还能表示抽象的规则、计划或事件。对象具有状态,一个对象用数据值来描述它的状态。对象还有操作,用于改变对象的状态,对象及其操作就是对象的行为。对象实现了数据和操作的结合,使数据和操作封装于对象的统一体中。

比如:人就是一个抽象的概念,它是类;但是具体到某一个人,比如说张飞,他就是一个对象,是一个具体的事物。

引用类型

C++里面多了一个叫引用的东西,这个东西要深刻理解我感觉只能理解为指针常量,然后我对它的所有操作都是对实际地址的值做操作,比如 swap 函数,在 C 语言种可能需要这么写:

1
2
3
4
5
void swap(int *a,int *b){
int t=*a;
*a=*b;
*b=t;
}

C++有了引用之后就可以这么写 swap 函数了:

1
2
3
4
5
void swap(int &a,int &b){
int t=a;
a=b;
b=t;
}

上下对比可以发现,如果我对变量声明为引用,那么我对此进行的一切操作将实时影响到实参,就跟 C 语言里面传个指针一模一样,只不过我们不需要再用 * 去访问变量了,并且上述代码分别在 C 和 C++ 中编译出来的结果几乎一样。

返回值为引用时,跟引用为参数时差不多,我们通过如下定义去接收一个引用类:

1
2
int &f();
int &k=f();

包括定义引用变量的时候,如果报出 “非常量引用必须为左值”,那么大概率是我在引用的时候引用了不能被修改变量,我们在引用返回的时候,不能是返回一个局部变量,因为它们的生命周期仅限于它们函数调用内部,调用结束即销毁。

跟指针一样,我对引用类型修改了也会影响到它本身引用的那个变量。

C++类定义

用 class 关键字定义一个类,默认会创建两个函数,一个是与类名一致的构造函数,一个是类名前面加上 ~析构函数,这两个函数分别会在创建对象、销毁对象时调用。

数据保护

类在某些情况下与 C 的结构体类似,只不过 C 的结构体不支持继承,成员函数之类的。 类中可以有如下几个标签,在标签里面定义的成员或者是函数都被标签的属性所设置,一般情况下有如下几个标签。

  • public:都可以访问
  • default:不同包之间不能访问
  • protected:类外不能访问
  • private:父子之间不能访问

我们创建新类的时候默认会给 public 和 private 这么两个标签。

C++的类有三个默认函数:

  • 默认构造函数,一般形式:classname();
  • 默认析构函数,一般形式:~classname();
  • 默认拷贝构造函数,一般形式:classname(classname &obj);

拷贝构造

主要有一个拷贝构造函数,它会根据已有的类来创建一个与之相似的类,并且默认是浅拷贝。拷贝构造函数会在简单的赋值的时候被调用,比如 int 类的赋值:a=b,我们理所当然地认为就是这样把值一模一样的复制过来,如果是一个类那么这个就有讲究了,有时候并不能直接一模一样地复制过来,我们也需要定义,当然如果是浅拷贝就是一模一样的复制过来,深拷贝就是对于动态分配内存的指针,采用相同的思路构造复制。

然后也是终于知道了拷贝比构造函数什么时候会被调用了,目前发现在传参时候会调用拷贝构造函数传参。

静态成员和静态方法

静态成员和静态方法不属于任何一个对象,他指的是整个类的属性。比如,有一个"人"类,那么人的总数对任何一个个体成员来说都是没有意义的,因为他对于整个 "人" 类才是有意义的。

静态方法不能引用非静态变量,很简单,因为静态方法是对整个类而言的,并不能影响到个体。

常函数

在函数定义后加一个 const 将函数定义为常函数,常函数不能修改成员变量,否则会报 “表达式必须是可修改的左值” 错误。但是我们可以添加修饰符 mutable 来取消个别成员对这个的限制

运算符重载

一般定义形式

1
type operator op(arglist); 

type 为运算结果类型,op 为重载的运算符,arglist为参数列表。

重载加法:

1
xia0ji233 operator + (const xia0ji233& obj) const

在这里,我们定义了 xia0ji233 类 + xia0ji233 类的加法,并返回一个新的 xia0ji233 类。

这里可以简单地申明,它们之间做加法就是让两个成员相加。

1
2
3
4
5
6
7
xia0ji233 xia0ji233::operator+(const xia0ji233& obj) const
{
xia0ji233 temp;
temp.num1 = this->num1 + obj.num1;
temp.num2 = this->num2 + obj.num2;
return temp;
}

我们不仅可以重载同类的加法,我们还可以重载不同类之间的加法应该怎么办,甚至返回什么类型都可以自己定义的,运算符重载可以类外重载也可以类内重载,上面的例子是类内重载,如果是类外重载则需要加一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//xia0ji233 xia0ji233::operator+(const xia0ji233& obj) const
//{
// xia0ji233 temp;
// temp.num1 = this->num1 + obj.num1;
// temp.num2 = this->num2 + obj.num2;
// return temp;
//}
xia0ji233 operator + (const xia0ji233& obj1,xia0ji233& obj2) {
xia0ji233 temp;
temp.num1 = obj1.num1 + obj2.num1;
temp.num2 = obj1.num2 + obj2.num2;
return temp;
}

运算符重载可以重载很多运算符,但是重置自增(++),自减(--)略微有点麻烦。

前置的自增自减很简单,只需要对对应属性操作完成之后,返回那个对象即可,我们就可以这样写:

1
xia0ji233 &operator ++();

然而我们需要重置后置自增自减就需要先返回一个中间变量再去加上原来的对象了,这个时候就不能传引用,就是普通类型返回,我们在实现的时候可以先返回本对象,再为本对象操作,达到先返回,再修改的目的。

1
xia0ji233 operator ++(int);

但是我们参数需要传一个 int,因为可能会导致定义不下来。

如果我们需要让对象能直接被 cout 输出,我们需要重载 << 运算符,但是此时我们必须全局声明,不能再类内声明,并且第一个参数我们要指定为 cout 对象。

1
void operator << (ostream &cout, xia0ji233 obj)

cin 同理即可

1
2
3
4
void operator >> (istream& cin, xia0ji233& obj) {
cin >> obj.num1;
cin >> obj.num2;
}

因为我们输入需要改变本身,所以这里我们需要传个引用进去。

but又出现了一个问题,就是我如果想在后面接着跟上某个输出,比如 cout<<obj<<endl,会发现报错了,我们解决的方法可以让前面的运算返回一个 cout 对象,然后后面的运算就正常了。

1
2
3
4
5
ostream & operator << (ostream &cout, xia0ji233 obj) {
cout << obj.num1 << endl;
cout << obj.num2 << endl;
return cout;
}

我们还可以对逻辑运算符重载,返回bool类型即可,可以自定义比较规则。

我们还可以对括号(())运算符重载,这样的话我们调用这个函数可以直接使用 obj() 的方式去调用,但是要和构造函数做区分,否则会命名冲突。

我们同样可以对下表运算符([])进行重载,比如 C++ 的 string 类实际上就是一个对象,为什么我们能用 [] 很轻松地访问单个字符,就是因为它已经重载了这个运算符。

最后还可以对赋值(=)运算符重载,这里我们尽量使用深拷贝去定义它,即:有指针对象时也动态分配把指针对象的值拷贝到被赋值的对象创建的指针当中。

实验代码:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "main.h"
using namespace std;
xia0ji233 operator + (const xia0ji233& obj1, xia0ji233& obj2);
ostream & operator << (ostream &cout, xia0ji233 obj) {
cout << obj.num1 << endl;
cout << obj.num2 << endl;
return cout;
}
void operator >> (istream& cin, xia0ji233& obj) {
cin >> obj.num1;
cin >> obj.num2;
}
void test() {
xia0ji233 s1(1, 2), s2(3, 4);
s1++;
cout << ++s2 << endl;
s1(3, 4, 5);
}
int main() {
test();
system("pause");

}

xia0ji233::xia0ji233()
{
cout << "No argument construct" << endl;
this->num1 = 10;
this->num2 = 20;
}

xia0ji233::xia0ji233(int num1, int num2)
{
this->num1 = num1;
this->num2 = num2;
}

xia0ji233::~xia0ji233()
{
}

xia0ji233::xia0ji233( xia0ji233& obj)
{
cout << "copy construct" << endl;
this->num1 = obj.num1;
this->num2 = obj.num2;
}

void xia0ji233::ShowNum()
{
cout << "num1:" << this->num1 << endl;
cout << "num2:" << this->num2 << endl;
}

xia0ji233 &xia0ji233::operator++()
{
this->num1++;
this->num2++;
return *this;
}

xia0ji233 xia0ji233::operator++(int)
{
xia0ji233 temp;
temp = *this;
this->num1++;
this->num2++;
return temp;
}

void xia0ji233::operator()(int a, int b, int c)
{
cout << "Add Value:" << a + b << endl;
}

//xia0ji233 xia0ji233::operator+(const xia0ji233& obj) const
//{
// xia0ji233 temp;
// temp.num1 = this->num1 + obj.num1;
// temp.num2 = this->num2 + obj.num2;
// return temp;
//}
xia0ji233 operator + (const xia0ji233& obj1,xia0ji233& obj2) {
xia0ji233 temp;
temp.num1 = obj1.num1 + obj2.num1;
temp.num2 = obj1.num2 + obj2.num2;
return temp;
}

运算符重载主要是对引用类需要很熟练我们才能灵活地重载各个运算符。