【类与对象】C++编程知识回顾与拓展 第一篇
前言
在步入研究生生涯以来,痛感自身编程知识与能力的不足,亟需重新回顾,并重点学习关于C++中面向对象、类、STL等关键知识,提高自身编程能力,故决定开启关于C++编程知识的专栏以供参考,希望能够起到有效的作用。注:本系列教程主要参考了C语言中文网、菜鸟教程以及部分CSDN博主的相关内容,无任何商业用途。
文章目录
一、类的概念与使用
1. 基本概念
- 为什么要使用类:C++相比于C语言最大的差别就在于增加了面向对象编程,而正是类和对象使得C++成为了能够面向对象的编程语言,是C++的核心特性;
- 类的核心概念:类用于指定对象的形式,是创建对象的模板,包含了数据表示法和用于处理数据的方法
一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化,每个对象都是类的一个具体实例。
- 类的特性:与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间
- 对象的特性:对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间
2. 定义方法
-
类的定义意味着什么:定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
-
类需要被定义:类是用户自定义的类型,如果程序中要用到类,必须提前说明,或者使用已存在的类(别人写好的类、标准库中的类等),C++语法本身并不提供现成的类的名称、结构和内容。
-
类如何定义:
- 以关键字 class开头,后跟类的名称
\qquad (1)class
:C++新增的关键字,专门用来定义类
\qquad (2)如下面的例子中的Student
:类的名称,首字符通常大写,与其他标识符区分- 类的主体是包含在一对花括号 { } 中
\qquad (1)成员变量:或者说类的属性(Property)
\qquad (2)成员函数:或者说类的方法(Method)- 分号或声明列表:分号同样是类定义的一部分,表示类定义结束了,不能省略。
- 类通常定义在函数外面
// 类的定义
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
};
3. 对象
- 类与对象:在类的核心概念处已提到,类提供了对象了蓝图,而对象是类的实例化
- 声明/创建对象:
- 类似于基本类型定义变量:
Student Caiyanxin; // 创建对象
- 创建对象时
class
关键字可加可不加,通常省略:class Student Caiyanxin; //正确 Student Caiyanxin; //同样正确
- 创建对象数组:数组中的每个元素都是对应类的对象
Student bad_student[100];
重点:创建对象的两种方式:栈/堆
Student stu;
Student *pStu = &stu;
- 此处创建的对象在栈上分配内存,可以通过
&
获取地址,并通过指针指向该对象;
栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;
Student *pStu = new Student;
- 通过
new
创建的对象在堆上分配内存,此时这个对象是匿名的,无法直接使用,必须使用一个指针变量类接收一个指向该对象的指针,通过指针访问对应成员
内存由程序员管理,对象使用完毕后可以通过 delete 删除
- 成员访问:成员即包括成员函数和成员变量
1. 使用
.
点号访问:类似于结构体变量访问成员// 创建对象 Student Caiyanxin; // 访问对象成员 Caiyanxin.name = "caiyanxin"; Caiyanxin.age = 22; Caiyanxin.score = 98.5f;
2. 使用对象指针:通过
->
来访问,类似于结构体指针访问// 创建对象 Student *pStu = new Student; // 访问对象成员 pStu -> name = "winter"; pStu -> age = 18; pStu -> score = 99.5f; pStu -> say(); delete pStu;
二、成员函数与成员变量
1. 基本概念(内联函数)
- 成员变量:把定义写在类定义内部的变量,和普通变量一样,也有数据类型和名称,占用固定长度的内存,但是在定义类时不能对成员变量赋值。
(注意:类只是一种数据类型/模板,本身不占用内存,变量的值一定需要内存存储)
- 成员函数:把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样,是类的一个成员,可以操作类的任意对象,可以访问对象中的任意成员
函数类型\属性 | 独立性 | 作用范围 |
---|---|---|
类成员函数 | 类的成员,出现在类体中 | 由类决定 |
普通函数 | 独立的 | 全局/某个命名空间 |
- 函数定义的区别:类体内/类体外
- 在类体内定义成员函数:默认自动成为内联函数,无需使用inline标识符,不需要在函数名前面加上类名
- 在类体外定义成员函数:必须在函数名前面加上类名限定,再使用范围解析运算符/域解析符
::
来连接类名和函数名,指明当前函数属于哪个类
- 推荐/标准格式:在类体内作原型声明,在类外定义,类体的位置应在函数定义之前
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(); //函数声明
};
//函数定义
void Student::say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
- 内联函数:在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方,可以通过类内定义或在类外加inline关键字定义
不推荐在类体外定义inline函数的原因:
- 在类外定义内联函数必须将类的定义和成员函数的定义都放在同一个头/源文件中,否则编译时无法进行嵌入(将函数代码的嵌入到函数调用处)
- 对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
- 内联函数的特点:
- 只有当函数只有 10 行甚至更少时才将其定义为内联函数,为了用空间代价换区时间效率
- 在内联函数内不允许使用循环语句和开关语句;
- 内联函数的定义必须出现在内联函数第一次调用之前;
- 类结构中所在的类说明内部定义的函数是内联函数。
- 如果已定义的函数多于一行,编译器会**忽略 inline **限定符
2. 访问权限
- 访问权限:是否能够使用类内的成员,通过对类主体内部对各个领域标记来指定
- 成员访问限定符:
public
、protected
、private
,三个关键字来控制成员变量和成员函数的访问权限,分别表示公有的、受保护的、私有的
【注意:只有类的成员可被修饰,类不可被修饰,没有公有私有之分】
公有成员 public:
-
概念:在程序中类的外部可访问,可以不是用任何成员函数来设置和获取公有变量的值
-
实际操作特点:在公有区域定义相关的函数,方便在类的外部可以调用这些函数
私有成员 private:
-
概念:在程序中类的外部不可访问,且不可查看,只有类和友元函数可以访问私有成员
-
特性:在默认情况下类的所有成员都是私有的
-
实际操作特点:在私有区域定义数据,防止数据在外部修改和泄露,常常将成员变量以
m_
开头,与成员函数中参数区别开来
受保护成员 protected:
- 概念:在程序中类的外部不可访问,但是在派生类/子类中可访问(私有成员不可),具体会在类的继承中介绍
- 书写格式:此处对三类成员修饰通用
- 修饰符的修饰成员次序任意,既可以先写private部分,也可以先写public部分
- 一个类体中一种修饰符可出现多次,每个部分的有效范围到出现到另一访问修饰符或类体结束时为止
- 实际编程中为使程序清晰,应该使每一种修饰符在类体中只出现一次
- 实例:
#include <iostream>
using namespace std;
class Box
{
public:
double length;
void setWidth( double wid );
double getWidth( void );
private:
double height;
protected:
double width;
};
// 成员函数定义
double Box::getWidth(void)
{
return width ;
}
void Box::setWidth( double wid )
{
width = wid;
}
// SmallBox 是派生类
class SmallBox:Box
{
public:
void setSmallWidth( double wid );
double getSmallWidth( void );
};
// 子类的成员函数
double SmallBox::getSmallWidth(void)
{
return width ;
}
void SmallBox::setSmallWidth( double wid )
{
width = wid;
}
// 程序的主函数
int main( )
{
Box box;
// 不使用成员函数设置长度
box.length = 10.0; // OK: 因为 length 是公有的
cout << "Length of box : " << box.length <<endl;
// 使用成员函数设置高度
// box.height = 10.0; // Error: 因为 height 是私有的
// 使用成员函数设置宽度
box.setWidth(10.0); // OK: height 是受保护的,不能直接访问
cout << "Width of box : " << box.getWidth() <<endl;
SmallBox box;
// 使用成员函数设置宽度
box.setSmallWidth(5.0);
cout << "Width of box : "<< box.getSmallWidth() << endl;
return 0;
}
3. 静态成员与const成员
静态成员变量:
-
目的/本质特性:多个对象共享数据,即无论创建多少个对象,静态成员变量都只有一个副本
-
创建:使用关键字
static
修饰 -
关键特性:
- static 成员变量属于类,不属于某个具体的对象,创建多个对象时也只分配一份内存;
- static 成员变量必须在类声明的外部初始化,且如果不赋值,那么会被默认初始化为0
- 静态成员变量在初始化时不能再加 static,但必须要有数据类型。被
private、protected、public
修饰的静态成员变量都可以用这种方式初始化。- static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
- static 成员变量既可以通过对象来访问,也可以通过类来访问,但要遵循
private、protected、public
关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。- static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问
- static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。
- 全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的。
静态成员函数:
- 目的/特性:只能访问静态成员,也只为了访问静态成员
- 创建:使用关键字
static
修饰 - 关键特性:静态成员函数在声明时要加 static,在定义时不能加 static;静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用
- 与普通成员函数的差别:没有 this 指针,只能访问静态成员,不需要对象进行调用,关于this指针在本文后面有细致讲述
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
public: //声明静态成员函数
static int getTotal();
static float getPoints();
private:
static int m_total; //总人数
static float m_points; //总成绩
private:
char *m_name;
int m_age;
float m_score;
};
int Student::m_total = 0;
float Student::m_points = 0.0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++;
m_points += score;
}
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义静态成员函数
int Student::getTotal(){
return m_total;
}
float Student::getPoints(){
return m_points;
}
int main(){
// 通过类类访问 static 成员变量
Student::m_total = 10;
// 通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
// 通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
// 使用匿名函数
(new Student("小明", 15, 90.6)) -> show();
(new Student("李磊", 16, 80.5)) -> show();
(new Student("张华", 16, 99.0)) -> show();
(new Student("王康", 14, 60.8)) -> show();
int total = Student::getTotal();
float points = Student::getPoints();
cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl;
return 0;
}
const成员:
- const成员变量:保护数据,使用
const
加以限定
- 用法:与普通const变量相似,只需要在声明时加上 const 关键字;
- 初始化const成员变量:通过构造函数的初始化列表进行,具体可见下篇博客;
- const成员函数:可以使用类中的所有成员变量,但是不能修改它们的值
- 用法:必须在声明和定义的时候在函数头部的结尾加上 const 关键字
- 通常将 get 函数设置为常成员函数。读取成员变量函数的名字通常以get开头,后跟成员变量的名字
class Student{
public:
Student(char *name, int age, float score);
void show();
//声明常成员函数
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义常成员函数
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
- const 修饰用法:
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改
例如:const char * getname()
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量值,而不能修改
例如:char * getname() const
const对象:
- 常对象:只能调用类的const成员,使用const修饰
// 定义常对象
const class object(params);
class const object(params);
// 定义const指针
const class *p = new class(params);
class const *p = new class(params);
// class为类名,object为对象名,params为实参列表,p为指针名
4. 友员函数与友员类
- 目的:借助友元,使 其他类的成员函数以及全局范围内的函数访问当前类的private成员
友元函数:
-
用法:使用
friend
关键字修饰,可以是不属于任何类的非成员函数(类以外定义),也可以是其他类的成员函数,都可以在类中声明; -
作用:友元函数可以访问当前类中的所有成员,包括 public、protected、private 属性;
\qquad (1)将非成员函数声明为友元函数:
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
public:
friend void show(Student *pstu); //将show()声明为友元函数
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//非成员函数
void show(Student *pstu){
cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl;
}
int main(){
Student stu("小明", 15, 90.6);
show(&stu); //调用友元函数
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //调用友元函数
return 0;
}
- 上述代码中
show()
函数为全局范围的非成员函数,不属于类,需要调用student类中的private成员,故需要声明为友元函数- 友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象
- 成员函数在调用时会隐式地增加 this 指针,指向调用它的对象,从而使用该对象的成员;而 show() 是非成员函数,没有 this 指针,编译器不知道使用哪个对象的成员,要想明确这一点,就必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或对象引用),并在访问成员时指明对象。
\qquad (2)将其他类成员函数声明为友元函数:
#include <iostream>
using namespace std;
class Address; //提前声明Address类
//声明Student类
class Student{
public:
Student(char *name, int age, float score);
public:
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//声明Address类
class Address{
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
public:
Address(char *province, char *city, char *district);
//将Student类中的成员函数show()声明为友元函数
friend void Student::show(Address *addr);
};
//实现Student类
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}
//实现Address类
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
int main(){
Student stu("小明", 16, 95.5f);
Address addr("陕西", "西安", "雁塔");
stu.show(&addr);
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
- 由于在address类定义前使用了该类,故需要提前声明
- 一般情况下类必须要在正式声明后使用,但是有些时候提前声明也可以先使用,但是只有在正式声明之后才能创建对象(内存原因)
- 一个函数可以被多个类声明为友元函数,就可以访问多个类中的 private 成员
友元类:
- 目的:将整个类声明为友元类,友元类中的所有成员函数都是另外一个类的友元函数
- 用法:一般不建议将整个类声明为友元,保证安全
- 关键:友元的关系是单向的而不是双向,友元的关系不能传递
三、类的封装、this指针、与结构体的区别
1. 类封装
- 基本概念:尽量隐藏类的内部实现,只向用户提供有用的成员函数
- 设计规范:
- private: 实际项目开发中的成员变量以及只在类内部使用的成员函数
(只被成员函数调用的成员函数,对外部无影响)- public:允许通过对象调用的成员函数
-
赋值思想:额外添加两个 public 属性的成员函数,set函数用于给成员变量赋值,get函数用于读取成员变量的值
-
初始化成员变量:创建对象时调用构造函数
2. this指针
-
概念:一个const指针,指向当前对象,是所有成员函数的隐含参数
-
用处:在成员函数内部,它可以用来指向调用对象从而访问对象的所有成员,而每一个对象都能通过 this 指针来访问自己的地址
#include <iostream>
using namespace std;
class Student{
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
void Student::show(){
cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}
int main(){
Student *pstu = new Student;
pstu -> setname("李华");
pstu -> setage(16);
pstu -> setscore(96.5);
pstu -> show();
return 0;
}
- 特点:
- 只能用在类的内部,通过 this 可以访问类的所有成员(包含私有成员)
- 可用于区分成员变量和重名的函数参数
- this 是一个指针,要用
->
来访问成员变量或成员函数- 只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值,因此不能在 static 成员函数中使用
- this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
- this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的
- 本质:成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this,不过是隐式的,并不出现在代码中,而是在编译阶段由编译器将它添加到参数列表中,从概念上理解其实类似于*成员函数的局部变量,是成员函数和成员变量关联的桥梁。
3. 类与结构体的区别
- 基本上通用,区别可见下表:
关键字 | 成员默认属性 | 默认继承方式 | 模板是否可用 |
---|---|---|---|
class | private | private | 可以 |
struct | public | public | 不可以 |
后记
内容: 本篇博客主要介绍了C++中面向对象概念的关键:类与对象,其中详细的介绍了类的基本概念、定义方法以及对象的使用,并且从成员函数与成员变量的概念、访问权限出发,提及了内联函数、静态成员、const成员与友员函数/类的使用方法,最后对类的封装、this指针以及与结构体之间的区别,深入浅出地从各个角度分析了类的使用方法,在下篇博客中将会主要针对于构造函数、析构函数与拷贝的讲解,敬请期待