C++ 类与对象(一)

一、面向过程与面向对象编程

 我们经常听别人谈起C语言是面向过程的编程语言,而C++是面向对象的编程语言。但是你让他具体说下什么是面向过程、什么是面向对象可能他很难说清楚。今天鄙人拙见谈谈什么是面向过程、什么是面向对象的看法。不管是面向什么都要把程序写出来,那程序是什么?计算机编程领域的祖师爷尼古拉斯•威茨曾经说过程序=数据结构+算法所以程序的本质:为了解决某个问题,选择合适的数据结构和算法进行若干步骤的求解。对于一个问题求解的思路有很多种,我认为面向过程与面向对象编程的区别就是对问题分析求解的思想不同

面向过程编程:将一个大问题分而治之划分成若干个小问题。每个小问题需要做某些操作并对数据进行某些处理,所以我们定义函数来解决这些小问题,并在函数里面对数据进行处理。我们会发现在面向过程的思想一直围绕解决若干个小问题,每个小问题对数据操作处理不一样。所以数据与函数是分开定义的,不能很好的结合在一起。所以在C语言中数据定义在结构体中,在函数中操作数据。对每个小问题通过不同函数来解决,最终合并起来解决整个大问题,所以我们常常听说面向过程编程就是面向函数编程。

面向对象编程:道教中有句名言:道生一,一生二,二生三,三生万物。说明万物相互之间是有联系的。面向对象思想采纳了道教的观点,认为不管是看待世界还是看待问题,都是由具有行为的各种对象组成的,不同类型对象之间互相有着联系每个对象具有某种属性和行为。例如人是一个对象,那人的名字、年龄、身高、体重等都为人这个对象的属性。人可以做好事情,比如吃饭、喝水、学习、睡觉等都为人这个对象的行为。我们发现面向对象思想将属性和行为做为一个不可分割的整体,即数据和处理数据的函数不可分割看待。 人这个对象可以有很多个,例如:张三、李四、王五。每一个具体对象都有这些相同的属性和行为所以抽象出类这个概念。即类是描述对象属性和行为的,是一个抽象的概论,而对象是类的实例化,是有生命的。

 上面是我认为的什么是面向过程,什么是面向对象,总结一句话就是看待问题、解决问题的思想不同。 我们不能说哪一种好哪一种不好,每个人可以根据自身对问题看法选择。本人不认同C语言就一定是面向过程的编程语言,C++就一定是面向对象的编程语言的说法。只能说C语言之父丹尼斯·里奇在语言设计时偏向于面向过程的思想。C++之父本贾尼在语言设计时偏向于面向对象的思想。导致语言层面对不同思想支持力度不同。如果有人问C语言一定不能面向对象编程或C++一定不能面向过程编程那一定是个刚入门的学习者。

另外说明的是,C++的设计思想是面向对象的,但是C++是站在C语言的肩膀上的,对C语言进行了兼容,所以我认为C++是基于面向对象的,而不是像Java一样全面向对象的

二、C++对C语言struct的兼容

上文中说过C++是站在C语言的肩膀上的,对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
#include <iostream>
using namespace std;

struct Person
{
char name[10];
int age;
};

void Print(struct Person* p)
{
printf("姓名:%s 年龄:%d\n", p->name, p->age);
}

int main()
{
struct Person p1 = { "张三",18 };
struct Person p2;
strcpy(p2.name, "李四");
p2.age = 19;
Print(&p1);
Print(&p2);
return 0;
}

输出

1
2
姓名:张三 年龄:18
姓名:李四 年龄:19

C++中struct的本质就是class,所以可以在struct中定义函数

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
struct Person
{
char _name[10];
int _age;

void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
printf("姓名:%s 年龄:%d\n", _name, _age);
}
};


int main()
{
struct Person p1 = {"张三",18};
Person p2;
p2.Init("李四", 19);
p1.Print();
p2.Print();
return 0;
}

输出

1
2
姓名:张三 年龄:18
姓名:李四 年龄:19

C++中structclass的唯一区别在下文中有讲解

三、类的定义

 C++面向对象的思想,认为对象由属性和行为组成,不能分割看待,并将属性和行为相同的对象抽象出类这个概念。属性和行为都在类里面。在类中定义的属性称为成员变量,类中定义的行为称为成员方法。

语法:class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。

1
2
3
4
5
class 类型
{
属性(成员变量)
行为(成员方法)
};

根据是否在类中定义成员方法将类的定义分为两种

3.1 成员方法的定义在类里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
char _name[10];
int _age;

void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}
void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

3.2 成员方法的定义不在类里面

在项目中推荐这种方式定义类,日常测试时则可以任选一种
Person.h 头文件

1
2
3
4
5
6
7
8
class Person
{
char _name[10];
int _age;

void Init(const char* name, int age); // 只在类中声明成员方法
void Print();
};

Person.cpp 源文件

1
2
3
4
5
6
7
8
9
10
void Person::Init(const char* name, int age) // 成员方法的定义 Person为类型 :: 为作用域限定符
{
strcpy(_name, name);
_age = age;
}

void Person::Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}

在成员方法的定义时,需要用类型+作用域限定符指明是哪一个作用域里面函数,因为一个类相当于一个作用域

四、类的封装性

 面向对象思想中,对象由属性和行为构成。并且不同类型对象之间相关联系。对象之间相互联系并不代表说我一定要把我这个对象的所有细节对你展开。例如:我这个对象实现了吃饭这个行为,另外一个对象实现了开车这个行为。假设存在开车需要先吃饭的联系,你只要开车之前先吃饭就行,不用管这个对象到底如何吃饭的、吃了什么。在面向对象思想中引入封装性这个概念,即隐藏对象实现的具体细节,只对外提供调用接口。封装是通过访问限定符实现的

4.1 访问限定符

访问限定符有三种:public、protected、private 三种类型

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(protected与private的区别在于继承时有所不同)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 访问限定符是基于类而不是基于对象的,同一个类的所有对象可以互相访问
  5. class的默认访问权限为private,struct为public(因为struct要兼容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
class Person
{
private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print1()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}

void Print2(const Person& p) // 同一个类的所有对象可以互相访问,所以可以访问p的私有成员
{
cout << "姓名:" << p._name << " 年龄:" << p._age << endl;
}
};



int main()
{
Person p1, p2;
p1.Init("李四", 19); // 正确,因为成员方法Init访问限定符为public, 类外面可以访问
p2.Init("张三", 18); // 正确,因为成员方法Init访问限定符为public, 类外面可以访问
p1.Print1(); // 正确,因为成员方法Print1访问限定符为public, 类外面可以访问
p1._age = 10; // 错误,因为成员变量_age访问限定符为private, 类外面不可以访问

p1.Print2(p2); // 正确,因为成员方法Print2访问限定符为public,类外面可以访问

return 0;
}

4.2 struct与class区别

structclass都是定义类的关键字,唯一的区别class的默认访问权限与继承方式为private,struct为public(因为struct要兼容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
struct Person
{
char _name[10];
int _age;

void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
printf("姓名:%s 年龄:%d\n", _name, _age);
}
};

int main()
{
Person p1;
p1.Init("李四", 19); // 正确,因为成员方法Init访问限定符没有明确定义,所以默认为public, 类外面可以访问
p1.Print(); // 正确,因为成员方法Print访问限定符没有明确定义,所以默认为public, 类外面可以访问
p1._age = 10; // 正确,因为成员变量_age访问限定符没有明确定义,所以默认为public, 类外面可以访问
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
class Person
{

char _name[10];
int _age;

void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1;
p1.Init("李四", 19); // 错误,因为成员方法Init访问限定符没有明确定义,所以默认为private, 类外面不可以访问
p1.Print(); // 错误,因为成员方法Print访问限定符没有明确定义,所以默认为private, 类外面不可以访问
p1._age = 10; // 错误,因为成员变量_age访问限定符没有明确定义,所以默认为private, 类外面不可以访问
return 0;
}

五、对象

用类创建对象的过程,称为类的实例化

●类是描述对象属性和行为的,是一个抽象的概论,而对象是类的具体实现是有生命的。类是不占内存空间的,而实例化出的对象是占内存空间的。可以把类当作建筑物图纸,对象是根据设计图具体建出来的房子
●一个类可以实例化出多个对象,每个对象占用不同内存空间

5.1 对象的大小

 对象所占内存空间的大小计算关键看对象里面存放了什么。对象是类的实例化,类由成员变量和成员方法组成。每个对象的属性不同,比如我叫张三,你叫李四。但是行为都是相同的,大家都要吃饭、睡觉等。每个对象中有没有存放相同成员方法成为计算对象大小关键
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{

private:
int i;
int j;

public:
void Print()
{
cout << "i = " << i << "j = " << j << endl;
}
};

int main()
{
A a;
cout<< sizeof(a) << endl;

return 0;
}

输出

1
8

 每个对象中成员变量是不同的,但是调用相同的成员函数(传入隐藏的this参数区分不同对象),如果按照此种方式一存储,当一个类创建多个对象时,每个对象中都会保存一份相同代码,浪费空间,所以采用方式二存储:一个对象的所占内存空间大小,是该对象中所有”非静态成员变量”所占空间大小之和(有虚函数时还有加上虚函数表指针大小)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
public:
void Print()
{
cout << "Hello"<< endl;
}
};

int main()
{
A a;
cout<< sizeof(a) << endl;

return 0;
}

输出

1
1

空类(类中没有成员变量)实例化出来的对象大小为1字节。因为空类比较特殊,实例化出来的对象没有存储任何东西,但是为了表示这个对象,编译器给了空类的对象一个字节来唯一标识

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 Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1;
cout<< sizeof(p1) << endl;

return 0;
}

输出

1
16

 大家会觉得Person类实例化出来对象大小为14字节,因为_name为10字节,_age为4字节加起来就14字节,但结果是16字节。因为对象中成员变量也要进行内存对齐关于内存对齐可参考这篇链接链接

结论:
● 一个对象的所占内存空间大小,是该对象中所有“非静态成员变量所占空间大小之和(有虚函数时还有加上虚函数表指针大小)”
● 空类(类中没有成员变量)实例化出来的对象大小为1字节
● 对象中成员变量也要进行内存对齐

六、this指针

6.1 什么是this指针

 回答什么是this指针前先看下面这份代码

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
class Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1;
Person p2;
p1.Init("张三", 18);
p2.Init("李四", 20);
p1.Print();
p2.Print();

return 0;
}

输出

1
2
姓名:张三 年龄:18
姓名:李四 年龄:20

 Person类中有Init与Print两个成员方法,成员方法中没有关于不同对象的区分,那当p1调用Init函数时,该函数是如何知道应该设置p1对象,而不是设置p2对象呢?同理p2调用Print函数时,该函数是如何知道应该打印p2对象的属性,而不是打印p1对象的属性呢?

C++中引入this指针解决该问题,即: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
class Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age) //等同于 void Init(Person* const this, const char* name, int age)
{
strcpy(_name, name); // 等同于 strcpy(this->_name, name);
_age = age; // 等同于 this->_age = age;
}

void Print() //等同于 void Print(Person* const this)
{
cout << "姓名:" << _name << " 年龄:" << _age << endl; // 等同于cout << "姓名:" << this->_name << " 年龄:" << this->_age << endl;
}
};

int main()
{
Person p1;
Person p2;
p1.Init("张三", 18); // 等同于 p1.Init(&p1,"张三", 18);
p2.Init("李四", 20); // 等同于 p2.Init(&p2,"李四", 20);
p1.Print(); // 等同于p1.Print(&p1);
p2.Print(); // 等同于p2.Print(&p2);

return 0;
}

6.2 this指针的特性

6.2.1 成员方法不能显性定义this指针或调用时传入对象地址

1
2
3
4
5
6
// 错误,编译器会隐式定义this指针作为函数第一个形参,不能手动定义this指针做为形参
void Init(Person* const this, const char* name, int age)
{
strcpy(_name, name);
_age = age;
}
1
2
Person p1;
p1.Init(&p1,"张三", 18); // 错误,编译器自动会传入对象地址,不能手动传入

6.2.2 this指针不可修改

this指针类型为:类名* const this

1
2
3
4
5
6
void Init(const char* name, int age) 
{
this = nullptr; // 错误,this指针不可以修改
strcpy(_name, name); // 等同于 strcpy(this->_name, name);
_age = age; // 等同于 this->_age = age;
}

6.2.3 成员方法的形参不要与成员变量同名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person
{

private:
char name[10];
int age;

public:
/*
错误,成员方法的形参和成员变量同名,局部变量优先级更高,下面name和age都认为是形参的name和age
*/
void Init(const char* name, int age)
{
strcpy(name, name);
age = age;
}
};

一般建议成员变量名前面加下划线或在成员变量名后面加下划线,与成员方法中的形参做为区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name); // _name不是形参名,则编译器认为_name是成员变量,自动会加上this->,即 strcpy(this->_name,name);
_age = age; // 等同于 this->_age = age;
}
};

下面这种写法不推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{

private:
char name[10];
int age;

public:
/*
由于成员方法的形参和成员变量同名,局部变量优先级更高,为了避免下面name和age都认为是形参的name和age
我们用作用域限定符,指明哪个name、age是成员变量的name、age。但是不推荐这种写法,代码可读性、逻辑性较差
*/
void Init(const char* name, int age)
{
strcpy(Person::name, name); // Person::name 表示name是Person作用域中的name,因为一个类就是一个作用域
Person::age = age;
}

};

6.2.3 可以在非静态成员方法内显式使用this指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}
/*
在非静态成员方法内可以显性使用this指针,不过没有太大意义,因为编译器默认会用this指针访问成员变量
*/
void Init(const char* name, int age)
{
strcpy(this->_name, name);
this->_age = age;
}
};

6.3 this指针存放在哪里

 this指针本质上其实是一个成员方法的形参,是对象调用成员方法时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。因为this指针是形参,this指针存放在调用成员方法的栈中,但是部分编译器将this指针存放在寄存器中
在这里插入图片描述

七、 const 修饰类的成员函数

 类的成员函数的隐藏形参this指针类型为:类名* const this,表示this指针本身不能改变,但是this指针指向的对象可以改变,即对象的成员变量可以改变。有些时候成员变量不需要改变,为了防止误操作,可以在成员函数形参列表后面加上const,表示对隐藏形参this指针修饰,此时this指针类型为:const 类名* const this,表明不能对this指针指向的对象里面的成员变量进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
{

private:
char _name[10];
int _age;

public:
void Init(const char* name, int age)
{
strcpy(_name, name);
_age = age;
}
void Print() const // 使用const修饰
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};