目录
类与对象的基本认识
C语言是面向过程的编程语言,所谓面向过程,即处理好解决一个问题需要的步骤
而C++是面向对象的编程语言,所谓面向对象,即关心需要处理的问题中有多少个整体,每个整体之间有什么关系,通过对象和对象之间的联系交互解决问题
举一个生活实例,一个人洗衣服的方式
- 用手洗------需要关注下面的步骤
上面用手洗的方式就是面向过程的步骤,需要考虑到人洗衣服的每一个步骤
结合C语言面向过程,可以得到下面的解题步骤:
-
- 调用拿盆子的函数------函数实现拿盆子
- 调用放水的函数------函数实现放水
- 调用放衣服的函数------函数实现放衣服
- 调用放洗衣粉的函数------函数实现放洗衣粉
主函数中通过上面多个函数的顺序调用完成一系列的动作,如果没有某一个特定的函数,那么将无法完成相应的动作
- 用洗衣机洗------关注对象之间的关系
在洗衣服过程中,涉及到人、衣服、洗衣机和洗衣粉四个对象
而整个洗衣服的过程只有:人将衣服放入洗衣机,倒入洗衣粉,启动洗衣机,最后人晾衣服即可
在这个过程中,处理好:人和衣服,人和洗衣机,人和洗衣粉以及洗衣机和衣服之间的关系即可,不需要关注人是如何把衣服放入洗衣机中的步骤,人是如何启动洗衣机的步骤,人是如何放入洗衣粉的步骤以及洗衣机是如何将衣服洗干净的步骤这些细节问题
结合C++的面向对象,可以得到下面的分析:
-
- 人和衣服------人使用衣服中的方法完成拿衣服和晾衣服的动作,但是拿衣服和晾衣服具体是如何实现的与人无关
- 人和洗衣机------人使用洗衣机中的方法启动洗衣机,但是洗衣机内部启动的方法具体是如何实现的与人无关
- 人和洗衣服------人使用洗衣粉中的方法倒入洗衣粉,但是人不用关心洗衣粉进洗衣机的方法是如何实现的与人无关
💡
总结,C语言中的面向过程指只要涉及到解决问题的主要步骤就需要去实现对应的方法,不论这个方法在C++中是哪一个对象的,而在C++中,每一个对象只需要实现自己的方法,剩下的交给对象之间的交互完成即可
类和对象的基本概念
类
类的认识
在C语言中,结构体中只能定义变量,但是在C++中,结构体可以定义变量和函数
cpp
//C语言的单链表
typedef struct SList
{
int val;
struct SList* node;
}SList;
//单链表的实现方法
//...
//C++中的单链表
struct SList
{
int val;
SList* node;//C++中的结构体属于类的一种,所以可以直接使用类名创建变量
//单链表的实现方法
//...
};
但是在C++中,更喜欢用class
代表类,struct
更多还是结构体,所以上面代码改为
cpp
//C++中的单链表
class SList
{
int val;
SList* node;
//单链表的实现方法
//...
};
类的定义
在C++中,使用下面的语法进行类的定义
cpp
class className
{
//成员变量
//实现方法
};//注意不要遗忘分号
在类的定义语法中,class
为类定义的关键字,className
为类名,{}
中的内容为类的主体
类的内容一般称作成员,而类中的变量一般成为成员变量,类中的方法一般称为方法或者成员函数
📌
需要注意的是,成员变量可以出现在成员函数的上方也可以出现在成员函数的下方,因为编译器在寻找成员变量时是将类当做一个整体看待,此时不论是在成员函数的上方还是下方都能寻找到需要的成员变量
类的两种定义方式
类的声明和定义分开
在C++中,如果在大项目中,一般使用类的声明和定义分开的方式,即在头文件中声明,在实现文件中定义,但是需要注意下面的问题:
- 因为声明和定义分开处理,所以定义所在的文件需要引入类声明所在的头文件
- 当定义需要实现类中声明的方法时将看不到放在头文件中的类的成员变量,此时需要使用域作用限定符指定对应的类
代码实例:
cpp
//class_test.h
#pragma once
class Person
{
//实现方法
int test();
//成员变量
int age = 0;
int num = 0;
};
//class_test.cpp(方法定义文件)
#include "class_test.h"//需要包含类所在的头文件
int Person::test()//使用域作用限定符指定是哪个类中的方法
{
return age + num;
}
类的声明和定义放在一起
当项目不够大时,或者只是简单的实现时,可以考虑将类的声明和定义放在同一个文件中,需要注意的是,当成员函数的声明和定义在一起时,编译器可能会当做内联函数处理
cpp
class Person1
{
//实现方法
int test()
{
return age + num;
}
//成员变量
int age;
int num;
};
成员变量名的命名规则建议
当成员函数的定义在类中时,需要考虑到成员函数的形式参数(局部变量)和成员变量之间的关系
cpp
//成员变量命名
class test
{
int num1;
int num2;
public:
int add(int num1, int num2)
{
num1 = num1;
num2 = num2;
return num1 + num2;
}
};
#include <iostream>
using namespace std;
int main()
{
test t;
cout << t.add(1, 2) << endl;
return 0;
}
输出结果:
3
如果像上面的代码中,成员变量和add
函数中的局部变量相同,那么会因为局部优先原则导致成员变量并没有被赋值成功,如下图所示
而如果将成员变量和add
函数中的局部变量加以区分,则此时可以成功将局部变量的值存入成员变量中
cpp
class test
{
int _num1;
int _num2;
public:
int add(int num1, int num2)
{
_num1 = num1;
_num2 = num2;
return num1 + num2;
}
};
#include <iostream>
using namespace std;
int main()
{
test t;
cout << t.add(1, 2) << endl;
return 0;
}
输出结果:
3
结果如下图所示:
📌
上面的区分成员变量和形式参数的方式仅供参考,具体以环境要求为主
类的封装
面向对象的三大特性:封装、继承和多态
在C++中,类的封装表示将方法和成员变量进行结合,对外只通过公开的接口与其他对象进行交互
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用
cpp
class Person
{
//实现方法
int test();
//成员变量
int age = 0;
int num = 0;
};
在上面的代码中,test
函数并没有公开,因为访问权限的问题,所以外部对象将无法使用
类的访问限定符
在C++中,一共有三个访问限定关键字,即private
(私有)、protected
(受保护)以及public
(公开)
在C++中,类的访问权限有下面的特点
public
修饰的成员在类外可以直接被访问protected
和private
修饰的成员在类外不能直接被访问(此处protected
和private
是类似的,只研究private
与protected
和public
的区别,三者具体细节见后序分析)- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到
}
即类结束 class
的默认访问权限为private
,struct
为public
(因为struct
要兼容C语言)
cpp
//结构体默认是public,而类默认是private
#include <iostream>
using namespace std;
struct test
{
int val;
int test1()
{
cout << "结构体test1函数" << endl;
return val;
}
};
class test2
{
int val1;
int test3()
{
cout << "类test3函数" << endl;
return val1;
}
};
int main()
{
test t1; //结构体变量
t1.val = 0;//可以直接访问成员变量
t1.test1();//可以直接访问成员函数
test2 t2;
t2.val1 = 0;//无法直接访问,因为是private修饰的变量
t2.test3();//无法直接访问,因为是private修饰的函数
return 0;
}
报错信息:
"test2::val1": 无法访问 private 成员(在"test2"类中声明)
"test2::test3": 无法访问 private 成员(在"test2"类中声明)
需要访问类中的函数时,可以指定访问权限,使用public
后可以在类外访问
cpp
#include <iostream>
using namespace std;
struct test
{
int val;
int test1()
{
cout << "结构体test1函数" << endl;
return val;
}
};
class test2
{
public:
int val1;
int test3()
{
cout << "类test3函数" << endl;
return val1;
}
};
int main()
{
test t1; //结构体变量
t1.val = 0;//可以直接访问成员变量
t1.test1();//可以直接访问成员函数
test2 t2;
t2.val1 = 0;//无法直接访问,因为是private修饰的变量
t2.test3();//无法直接访问,因为是private修饰的函数
return 0;
}
输出结果:
结构体test1函数
类test3函数
cpp
#include <iostream>
using namespace std;
class test4
{
//private修饰val2,到public结束
private:
int val2 = 1;
//public修饰test5,到右大括号结束
public:
int test5()
{
cout << "类test4函数" << endl;
return val2;
}
};
int main()
{
test4 t;
t.val2;//无法访问,因为是private修饰
t.test5();//可以访问,因为是public修饰
return 0;
}
错误信息:
"test4::val2": 无法访问 private 成员(在"test4"类中声明)
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域
cpp
//class_test.h
#pragma once
class Person
{
//实现方法
int test();
//成员变量
int age = 0;
int num = 0;
};
//class_test.cpp(方法定义文件)
#include "class_test.h"
int Person::test()//使用域作用限定符指定是哪个类中的方法
{
//之所以能访问到私有变量age和num是因为指定了test函数为Person类中的方法
//一个类中可以访问到private变量
return age + num;
}
对象
类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
cpp
//无法直接使用类名创建对象,除非是静态变量
#include <iostream>
using namespace std;
class Person
{
public:
int age;
static int num;
};
int main()
{
Person::age = 0;//类在没有对象之前不可以直接使用非静态的成员变量
Person::num = 0;//静态变量可以直接用类名访问
return 0;
}
报错信息:
对非静态成员"Person::age"的非法引用
在上面的代码中,因为类中的变量只是声明,告诉编译器当前类有哪些成员变量,但是并不为其开辟空间,而静态变量在编译过程中会分配空间,故可以直接用类名进行访问
需要访问类中的非静态变量时,需要用类名实例化出对象,再用对象去访问类中的变量
cpp
#include <iostream>
using namespace std;
class Person
{
public:
int age;
};
int main()
{
Person p;//实例化
p.age = 10;
cout << p.age << endl;
return 0;
}
输出结果:
10
在上面的代码中,因为p
为Person
类的对象,故在编译时会为p
分配空间,此时p
对象的成员变量age
就有了存储空间
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
cpp
#include <iostream>
using namespace std;
class Person
{
public:
int age;
};
int main()
{
//Person类的三个对象
Person p;
Person p1;
Person p2;
p.age = 10;
p1.age = 20;
p2.age = 30;
cout << p.age << endl;
cout << p1.age << endl;
cout << p2.age << endl;
return 0;
}
输出结果:
10
20
30
类对象的大小
在计算类对象的大小时,和计算结构体大小的方式相同,并且都需要考虑到内存对齐的规则,如果类中有定义成员函数,则不会计算成员函数的大小,只会计算成员变量的大小
cpp
class Person
{
int age;
int num;
int test()
{
cout << "Person类的函数 " << age << " " << num << " " << endl;
}
};
int main()
{
cout << sizeof(Person) << endl;
return 0;
}
输出结果:
8
在类对象访问成员变量时,每一个成员变量属于各自的对象存放在栈区,而成员函数是所有对象公共的存放在公共代码段区
如果类中没有成员变量,即只包括成员函数或者是空类时,类的大小为1个字节,这一个字节是为了表示给类占位,标识这个类存在
cpp
class Empty
{
};
class NoneMember
{
void test()
{
cout << endl;
}
};
int main()
{
cout << sizeof(Empty) << endl;
cout << sizeof(NoneMember) << endl;
return 0;
}
输出结果:
1
1
this关键字
cpp
class Person
{
public:
int age;
int num;
void test()
{
cout << "Person类的函数 " << age << " " << num << " " << endl;
}
};
int main()
{
Person p1;
Person p2;
p1.age = 10;
p1.num = 10;
p2.age = 20;
p2.num = 20;
p1.test();
p2.test();
return 0;
}
输出结果:
Person类的函数 10 10
Person类的函数 20 20
在上面的代码中,对象p1
和对象p2
的成员变量age
和num
有自己的存储的空间,两个对象的成员变量不会冲突,并且尽管test
函数是公共的,在test
函数中age
和num
也对应着各自的对象
在C++中,有一个关键字this
,是C++编译器给每个"非静态的成员函数"增加的一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有"成员变量"的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成,所以上面的代码实际上是
cpp
//对于对象p1
void test(this*)
{
cout << "Person类的函数 " << this->age << " " << this->num << " " << endl;
{
//当p1对象在调用test函数时,,则变为
void test(&p1)
{
cout << "Person类的函数 " << p1.age << " " << p1.num << " " << endl;
}
//对于对象p2
//当p2对象在调用test函数时,,则变为
void test(&p2)
{
cout << "Person类的函数 " << p2.age << " " << p2.num << " " << endl;
}
但是上面的代码只是演示,实际过程中不可以显式得将this
作为函数的形参
this指针的特点
this
指针的类型:类类型* const
,即成员函数中,不能给this
指针赋值。- 只能在"成员函数"的内部使用
this
指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this
指针。this
指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx
寄存器自动传递,不需要用户传递- 一般情况下,this指针存储在栈区上或者寄存器
ecx
(VS编译器做出的优化)中
📌
注意,this
指针有时可以为空,有时不可以为空
- 当对象调用函数不需要实际进行解引用操作时,
this
指针可以为空指针 - 当对象调用的函数实际进行解引用操作时,
this
指针不可以为空指针,否则会出现空指针解引用错误
cpp
class test
{
private:
int val = 0;
public:
void test1()
{
cout << "测试函数1" << endl;
}
void test2()
{
cout << "测试函数2 " << val << endl;
}
};
int main()
{
test* p = nullptr;
p->test1();//空指针可以直接调用
(*p).test1();//尽管手动解引用,但是实际上编译器处理时只在需要显式解引用的地方才会进行解引用操作,故此时依旧不会出错
p->test2();//空指针调用会在获取val处出现空指针解引用问题
return 0;
}
在上面的代码中,二者均不会出现编译错误,但是对于调用test2
来说,因为test2
函数中涉及到访问成员变量的空间,而指针对象p
是空指针,没有实际的地址空间,导致成员变量val
的空间也不存在,从而出现空指针解引用错误,而之所以访问成员函数不需要解引用是因为成员函数并不在对象所在的空间中,不需要在对象的空间中找函数,而是直接去公共代码段中寻找