1、什么是类什么是对象,如何创建一个类?
(1)类的概念:
在生活中,我们通常用“类”这个字来形容具有共同特点的一类事物,如:人类、鼠类、犬类,也许在具体的行为或特征方面上,同类不同个体会具有差异化,但是在宏观的角度而言也具有一定的相似性;
例如:人类的个体可能会有性格上的差异,体重上的差异,但是都需要呼吸以维持声明,因此我们用人类来比拟c++中“类”这一概念的话,那么具体的某一个人就是这一个“类”中的一个对象;
(2)创建一个简单的类:
类和我们所熟知的数据类型并无差异,只是相对而言,我们可以更为自由灵活的去创建一个对象或者变量,而不再受数据类型本身的限制,因为对于一个复杂的个体而言,其所要储存的数据不可能只是单纯的int类型、float类型或者是char类型等等,有可能是他们之中几个的结合,或者说我们所要储存的东西甚至无法用单纯的用数据来描述,也就是“行为”,换句话就是“函数”;
那么创建一个类之前我们要先知道声明“类”所用的关键字:“class”
类的语法:
class类名{
访问修饰符:
成员变量、函数等;
};
#include<iostream>
using namespace std;
class People{
string name;
int age;
float weight;
double height;
};
如上述代码所示,我们已经创建了一个类,类名为“People”,其中的变量包括:string类型的name,int类型的age,float类型的weight,double类型的身高,这些变量我们称之为成员变量,也称为类的属性;
(3)访问权限修饰符
但是我们并不想让任何人都可以随意的访问这些属性,因此在类中我们可以利用三种访问修饰符对类中不同的变量进行修饰分别为:
①“public”:公共的,任何位置都有权限访问;
②“private”:私有的,只有当前类内有权限访问;
③“protected”:受保护的,类内和子类有权限访问;
注:在类的定义中,未被访问修饰符所修饰的成员变量或成员函数默认为private;
由于同一类别的不同对象会具差异化,也就是具有不同的属性,因此我们通常不会直接在类中去给属性赋值,而是在创建对象时去初始化其中的属性,那么我们来创建一个属于“People”类的对象:
#include<iostream>
using namespace std;
class People{
private:
int age;
float weight;
public:
string name;
double height;
};
int main(){
People Xiao_Ming;
}
此时,我们已经在主函数中,创建了一个属于“People”类的对象:“Xiao_Ming”,由于Xiao_Ming属于People类,因此,其具有People类中的所有属性和行为(什么是行为我们一会再说);
2、类的初始化
(1)类外直接进行赋值初始化
首先声明(1)~(4)的本质都是赋值,并不是真正意义上的初始化,具体原因在(5)中说明;
现在的Xiao_Ming是一个刚创建好的对象,我们需要对他的属性进行初始化(赋值),如果想要在类外调用类成员的话我们使用“.”来实现,具体语法:“对象名.成员变量”,而由于访问权限是“public”的成员变量只有name和height,因此我们在类外只能初始化public类型的成员变量:
#include<iostream>
using namespace std;
class People{
private:
int age;
float weight;
public:
string name;
double height;
};
int main(){
People Xiao_Ming;
//类外进行赋值初始化
Xiao_Ming.name = "Xiao_Ming";
Xiao_Ming.height = 175.9;
}
但此时出现了一个问题,被“private”修饰的变量无法在类外进行操作,但是我们创建对象的时候一定是在类外创建的,这就形成了矛盾,这就需要我们使用类的行为来修改属性;
(2)利用类的行为修改对象属性
类内的函数就可以看作是类的行为,由于被private修饰的变量只能在类内进行访问,然而我们无法在类内直接进行赋值操作,因此,我们可以在类中定义一个可以接收参数的函数用来修改类内成员变量的值,这就实现了用类的行为改变对象的属性,从而可以灵活的初始化我们所创建的不同对象;
#include<iostream>
using namespace std;
class People{
private:
int age;
float weight;
public:
string name;
double height;
//在主函数中接收初始化的参数用于给age和weight赋值
void people_initial(int n,float m){
age = n;
weight = m;
}
//可以打印输出age和weight的值
void people_display(){
cout << age << endl;
cout << weight << endl;
}
};
int main(){
PeopleXiao_Ming;
//类外进行赋值初始化
Xiao_Ming.name = "Xiao_Ming";
Xiao_Ming.height = 175.9;
//利用类的行为修改对象的属性
Xiao_Ming.people_initial(18,60);
//打印输出对象age和weight的值
Xiao_Ming.people_display();
}
并且,我们可以创建多个对象,再依次进行赋值,并打印输出:
#include<iostream>
using namespace std;
class People{
private:
int age;
float weight;
public:
string name;
double height;
//在主函数中接收初始化的参数用于给age和weight赋值
void people_initial(int n, float m){
age = n;
weight = m;
}
//可以打印输出age和weight的值
void people_display(){
cout << age << endl;
cout << weight << endl;
}
};
int main(){
People Xiao_Ming;
People Xiao_Li;
//类外进行赋值初始化
Xiao_Ming.name = "Xiao_Ming";
Xiao_Ming.height = 175.9;
Xiao_Li.name = "Xiao_Li";
Xiao_Li.height = "182.9"
//利用类的行为修改对象的属性
Xiao_Ming.people_initial(18, 60);
Xiao_Li.people_initial(18, 67);
//打印输出对象age和weight的值
Xiao_Ming.people_display();
Xiao_Li.people_display();
}
但是如果我们的对象有很多,我们不可能在主函数中依次进行赋值操作,去完成所有对象的初始化,于是c++的类为我们提供了一种更为便捷的初始化方式——构造函数;
(3)利用”构造函数“初始化变量
首先我们要清楚什么是构造函数,每当我们创建一个类的对象时,系统会默认调用构造函数对我们所创建的对象进行赋值初始化操作,如果我们没有在类中定义构造函数,那么系统则会调用默认的构造函数进行初始化,且构造函数没有返还值,故也不存在返还值类型一说;
构造函数的语法如下:
类名(参数){
初始化操作程序;
}
调用构造函数的语法如下:
类名 变量名(参数);
那么,我们利用这一方法来重新创建对象:
#include<iostream>
using namespace std;
class People{
public:
//构造函数
People(int n, int m, string x, int y){
//将传入的参数依次赋值给成员变量
age = n;
weight = m;
name = x;
height = y;
}
//成员变量
string name;
int height;
void people_display(){
cout << "name:" << name << endl;
cout << "age:" << age << endl;
cout << "weight:" << weight << endl;
cout << "height:" << height << endl;
}
private:
int age;
int weight;
};
int main(){
People Xiao_Ming(18, 60, "Xiao_Ming", 180);
Xiao_Ming.people_display();
}
对于该程序而言,我们自己定义了类中的构造函数,那么系统将不会调用默认构造函数,系统的默认构造函数是无参数构造类型的函数,也就是说其只负责在栈区开辟当前对象所需要的内存空间,不会进行赋值初始化等操作,而我们通过自己定义构造函数,从而完善了他的功能性
但是,假如现在我们要定义两个对象时,我们希望其中一个进行初始化,一个不进行初始化,那么我们按照之前的写法,如下所示:
int main(){
//Xiao_Ming进行初始化
People Xiao_Ming(18, 60, "Xiao_Ming", 180);
//Xiao_Li不进行初始化
People Xiao_Li;
}
此时编译器会对代码的第5行进行报错提醒:类People不存在默认构造函数,也就是所,系统找不到与之匹配的构造函数类型,因此想要完成目标,我们就需要利用c++中函数重载的特性;
(4)函数重载
什么是重载函数,在c++中我们可以定义多个名字相同的函数,但是其传输的参数的个数,类型,顺序不同,此时编译器会跟据调用该函数时,其所传入的参数的类型来判断具体调用哪个函数,函数重载具有以下几个特点:
c++允许在一个作用域中(一个{}中)的某个函数有多种定义;
在同一个作用域内可以声明几个功能类似的同名函数,但是这些同名函数的形参必须不同(顺序、类别、个数、const修饰)不可以仅仅通过返还值类型的不同来重载函数;
调用重载函数时,编译器通过把你所使用的参数类型与定义的参数进行比较,决定最合适的定义,该过程称作”重载决策“;
那么我们可以利用该特性,在类中定义多个构造函数,从而适应多种对象初始化的情况:
#include<iostream>
using namespace std;
class People{
public:
//构造函数1(有参数)
People(int n, int m, string x, int y){
age = n;
weight = m;
name = x;
height = y;
}
//构造函数2(无参数)
People(){
age = 0;
weight = 0;
name = "尚未填写";
height = 0;
}
//成员变量
string name;
int height;
void people_display(){
cout << "name:" << name << endl;
cout << "age:" << age << endl;
cout << "weight:" << weight << endl;
cout << "height:" << height << endl;
}
private:
int age;
int weight;
};
int main(){
//利用构造函数1创建对象Xiao_Ming
People Xiao_Ming(18,60,"Xiao_Ming",180);
//利用构造函数2创建对象Xiao_Li
People Xiao_Li;
Xiao_Ming.people_display();
Xiao_Li.people_display();
}
在这段代码中,我们用两种的初始化方式,创建了对象Xiao_Ming和Xiao_Li;
注意:
这里我们在调用构造函数2也就是无参构造的时候,我们的写法是
People Xiao_Li;
而不是:
People Xiao_Li();
因为,我们如果写作第二种形式的话,编译器会认为我们是在做函数声明,声明了一个返还值类型为People类的函数,而不会将其认为是在调用无参的构造函数
(5)初始化参数列表
首先,我们要明白初始化和赋值的区别,初始化是在变量声明(分配好空间)的同时就为该变量所处的内区域的内存空间给予一个确定的值,而赋值则是清除空间的旧值并用新的值做拷贝替换,换而言之,赋值操作是初始化的一部分,如下代码所示:
int main(){
//对a进行初始化
int a = 1;
//对b进行赋值
int b;
b = 3
}
就实际意义,二者没有任何的区别,但是从代码运行的角度,赋值操作的本质是拷贝,也就是将已有的值从常量区复制拷贝到新的空间中去(具体可参考此文章:内存管理),因此,赋值相较于直接进行初始化而言会多进行一步复制拷贝的操作,故对于一些比较统一的成员变量我们可以考虑统一用初始化参数列表进行初始化,这样效率更高;
在使用初始化参数列表时注意:
只能在构造函数中使用初始化参数列表,可以给所有成员初始化;
成员的初始化顺序与类中的声明顺序一致,而与参数列表中的顺序无关;
const类型的变量和引用类型的变量必须在初始化参数列表中完成;
其中最后一条的原因:因为一旦我们创建了一个对象其内部的值如果未初始化就会被不确定的值所覆盖,构造函数中只能进行赋值操作,而无论是const类型的变量还是引用类型的变量都是不能进行赋值的,所以只能在初始化参数列表中进行初始化操作;
初始化参数列表的语法:
//构造函数
类名():成员变量1(给变量1初始化的值),成员变量2(给变量2初始化的值){
构造函数内程序段;
}
那么假设,我们现在要写一段程序,用来记录一个人信誉积分系统,起初每个人都是100分,每犯一次错误,就会减少1分:
#include<iostream>
using namespace std;
class people{
private:
string name;
int score;
public:
people(string input):score(100){
name = input;
}
void make_mistake(){
score--;
}
void score_display(){
cout << name << "'s score is:" << score << endl;
}
};
intmain(){
peopleXiao_Ming("Xiao_Ming");
peopleXiao_Li("Xiao_Li");
peopleXiao_Zhang("Xiao_Zhang");
Xiao_Ming.make_mistake();
Xiao_Zhang.make_mistake();
Xiao_Zhang.make_mistake();
Xiao_Ming.score_display();
Xiao_Li.score_display();
Xiao_Zhang.score_display();
}
对应这里的实际情况,因为每个人姓名都是不同的,因此我们将其以形参的形式传入到构造函数中,并传参到name中进行赋值,而由于每个人的初始分数都是100,故放在初始化参数列表中进行直接初始化,从而高效的实现了这一程序;
3、析构函数
和构造函数的功能相对应,构造函数用于初始化类的对象,而析构函数则是用于清理释放掉生命周期已经结束的对象(对象声明处所在的{}内为其整个生命周期);
与构造函数相同的是,如果我们未在类中定义析构函数,那么在某一个对象的生命周结束时,系统会自动调用默认的析构函数释放其所占用的内存空间,但是系统只能自动清理栈区空间,如果我们在构造函数中对我们的对象进行初始化的时候使用new或malloc这类函数向堆区申请了额外空间的话,需要手动用delet或free来释放这部分的空间,因此,需要自己在类中定义析构函数,从而完成其功能;
对于析构函数,其语法为:
~类名(){
析构函数内程序段;
}
那么我们来看以下的程序:
#include<iostream>
#include<string.h>
using namespace std;
class people{
public:
people(const char*xh,const char*dh){
std_id = newchar[32];
strcpy(std_id,xh);
tel = newchar[32];
strcpy(tel,dh);
}
~people(){
delete[] std_id;
delete[] tel;
}
void inf_display(){
cout << "std_id:" << std_id << endl;
cout << "tel:" << tel << endl;
}
private:
char* std_id;
char* tel;
};
int main(){
people Xiao_Ming("1234567890","1234567890");
Xiao_Ming.inf_display();
}
4、静态成员
(1)静态成员变量
我们用关键字“static”修饰一个类中的成员变量时,我们称该变量是静态成员变量,通过普通的静态变量的特点我们可以知道,静态变量储存在全局区,生命周期是从程序开始到程序结束,也就是说对于同一个类的多个对象而言,其静态成员是公用的,并不会随对象的创建而分配内存,也不会随某一个对象释放时而释放内存;
静态变量初始化的时候可以赋初值,也可以不赋初值,其默认为0,但是如果对其赋值应该在类外进行初始化!
那么利用这一特点,我们可以利用静态成员变量来记录我们所创建的对象的个数,如下代码所示:
#include<iostream>
using namespace std;
class people{
public:
staticintnum_people;
people(){
num_people++;
}
people(string name, int age){
num_people++;
}
~people(){
num_people--;
{
};
//静态成员变量初始化
int people::num_people = 0;
int main(){
people Xiao_Ming();
people Xiao_Li("Xiao_Li",18);
cout << people::num_people << endl;
}
也就是每当我们的程序调用了构造函数时,说明该增加了一个该类的对象,就会对num_people加1,而每当程序调用了析构函数时,说明有一个对象被释放了,从而num_people减1;
而且这段程序中值得我们注意的是:我们在主函数的结尾,输出当前对象个数的时候,我们使用的是“::”而不是“.”,这就说明静态成员变量不属于某个对象,而是属于整个类的;
因此,对于静态成员变量需要注意的是:
静态成员变量属于整个类所有,所有对象共享其所属类的静态成员变量;
静态成员变量的初始化应该在类外进行;
可以通过类名直接访问公有(public)静态成员变量;
可以通过对象名直接访问公有(public)静态成员变量;
静态成员变量在程序内部位于全局区(静态区);
静态成员变量的生命周期不依赖于任何的对象,与程序的生命周期一致;
(2)静态成员函数(this指针)
用staitic来修饰类中的成员函数,该函数便称为静态成员函数,与静态成员变量一致,静态成员函数也是归类所属,而非是某一个对象;
对静态成员函数最重要的特点是其只能访问静态成员变量或其他的静态成员函数,原因在于其内部没有“this”指针;
“this”指针也被称作是"自引用指针",每当创建了一个对象的时候,系统会自动将this初始化指向当前对象,而当调用成员函数时,”this“也会作为一个隐含参数传入到函数中;
由于我们的程序时逐条执行的,因此就不存在这同时对多个对象进行操作的可能,那么在一段程序中,可能存在着多个同类的对象,当程序执行到某个位置的时候,想要知道当前正在操作的具体是哪个对象,就需要由this指针做指引,也就是说this指针所指向的就是当前正在被调用的对象,例如以下代码:
#include<iostream>
using namespace std;
class people{
public:
int age;
string name;
int weight;
void eat(){
weight++;
}
};
int main(){
people Xiao_Ming;
Xiao_Ming.eat();
}
首先我们创建了一个对象“Xiao_Ming”当我们调用其内部的成员函数eat()时,此时this指针指向了对象“Xiao_Ming”,此时,”this“作为隐含参数传入到eat( )当中,我们虽然写作“weight++”而实际所执行的是“this->weight++”或者是“Xiao_Ming.weight++”而对于静态成员变量或静态成员函数而言,其不属于任何一个对象,故其类型与this指针的类型不匹配,自然无法进行指向,故无法通过静态成员函数来访问非静态成员变量或函数(无this指针指向)
但是,静态成员函数的好处在于,可以将其视为一个全局的函数,我们可以通过类名对其访问,而不需要先创建一个对象再通过对象进行访问,如下所示:
#include<iostream>
using namespace std;
class people{
public:
static int num_people;
static void show_num{
cout << num_people << endl;
};
people(){
num_people++;
}
~people(){
num_people--;
}
};
int people::num_people = 0;
int main(){
people Xiao_Ming;
people Xiao_Li;
//直接通过类名访问静态成员函数
people::show_num();
}
5、友元
(1)友元的概念
我们知道,类中的成员变量或成员函数可以被我们用不同的访问修饰符进行修饰的,其包括public、private、protected三种,其中public是共有的,任何对象或函数都可以对其进行访问,但是对于private和protected:
private:只能由类内成员函数、友元访问;
protected:只能由类内成员函数、子类函数、友元访问;
因此,友元的作用就是帮助类外其他函数或其他类,来访问类内私有或受保护的成员变量或函数;
友元的关键字:friend
(2)类外的全局函数作为类的友元
如果我们希望类A外的函数a可以访问友元中的私有(受保护)成员,那么我们应该在类A中声明,a函数是类A的友元,代码如下:
#include <iostream>
using namespace std;
class A {
public:
int a;
private :
int b;
// 友元函数的声明
friend void fun();
};
//友元函数的类外实现
void fun() {
A s;
s.a = 1;
// 在全局函数fun()中访问了类A中对象s的私有成员变量b
s.b = 2;
cout << s.a + s.b << endl;
}
在这段程序中,我们可以看到在类A中,a是公共的,b是私有的,但是由于我们在类A中声明了,fun( )是类A的友元函数,因此即使fun是类外的函数,却依旧可以通过创建对象来访问b
(3)类A作为类B的友元
如果我们希望,类B的任意成员函数可以访问类A对象的私有成员,那么我们应该声明,类B是类A的友元,如下代码所示:
#include <iostream>
using namespace std;
class A {
public:
int a;
private :
int b;
// 友元函数的声明
friend class B;
};
class B {
public:
void fun() {
A s;
s.a = 1;
//在B的成员函数fun中访问了类A中对象s的私有成员b
s.b = 2;
cout << s.a + s.b << endl;
}
void fun1() {
A s;
s.a = 1;
//在B的成员函数fun1中访问了类A中对象s的私有成员b
s.b = 2;
cout << s.a * s.b << endl;
}
};
在这段程序中,我们将类B做为类A的友元,可以看到此时类B的任意成员函数均可以访问类A对像的私有成员变量
(4)类B中的成员函数作为类A的友元
假如,我们只希望类B中特定的成员函数可以去访问类A的私有成员变量,那么我就不能将整个类B作为类A的友元,而是把特定的成员函数作为友元来声明,如下代码所示:
#include <iostream>
using namespace std;
class B {
public:
void fun();
};
class A {
public:
int a;
private:
int b;
friend void B::fun();
};
void B::fun() {
A s;
s.a = 1;
s.b = 2;
cout << s.a + s.b << endl;
}
在这段代码的写法上,有两点值得我们注意:
我们将类A的定义写在了类B的下面,如果我们写作了下述代码形式的话,由于编译器是顺序编译,因此fun( )函数的声明应该写在friend关键字声明之前,否则会出现编译器先看到了友元的声明却找不到函数的情况;
#include <iostream>
using namespace std;
class A {
public:
int a;
private:
int b;
friend void B::fun();
};
class B {
public:
void fun();
};
void B::fun() {
A s;
s.a = 1;
s.b = 2;
cout << s.a + s.b << endl;
}
其次,我们将fun( )函数的定义写在了类外而不是和声明写在一起,其原因与第一条类似,如果我们将代码写作如下形式的话,会出现编译器编译fun( )的时候,尚不知道fun( )是类A的友元,从而使得fun( )依旧无法访问类A的私有成员变量;
#include <iostream>
using namespace std;
class B {
public:
void fun(){
A s;
s.a = 1;
s.b = 2;
cout << s.a + s.b << endl;
}
};
class A {
public:
int a;
private:
int b;
friend void B::fun();
};
6、常成员变量和函数
(1)常量、常成员变量、常成员函数
当一个普通变量被const所修饰时,其数据类型变为了“常量”,意为:不可更改,不可赋值,也就是一个“只读”的量,同理,如果我们用const修饰一个普通成员变量或成员函数时,其变为了常成员变量,和常成员函数,这也就意味着,只有普通成员函数才能作为常成员函数,而像:构造函数、析构函数等需要对类的对象中的成员变量进行赋值操作的函数均不可成为常成员函数;
(2)常成员函数的内部函数是常指针类型
除此之外,我们需要知道的时,常成员函数不能修改成员函数的值,对于常成员函数而言其内部的”this“指针类型为常指针类型,如下代码所示:
#include <iostream>
using namespace std;
class student {
public:
// 声明常成员变量
int score;
// 构造函数定义
student(int score) {
this->score = score;
}
// 常成员函数定义
void score_change(int score) const{
int a = 1;
this->score = score;
}
};
int main() {
student xiao_ming(89);
xiao_ming.score_change(90);
return 0;
}
如果我们运行该段代码,会出现如下的报错:ERRO:C3490由于正在通过常量对象访问”score“因此无法对此修改;
我们观察代码中的第14行,此时的”this“指针在常成员函数中,其类型为常指针类型(const class*类型),那也就意味着其所指向的值无法进行修改,也就无法作为等号的左值进行赋值操作;
同理,常成员函数只能调用常成员函数,无法调用未被const修饰的成员函数