从今天起开始进入C++核心编程阶段的学习,也就是学习面向对象编程思想,探讨C++中的核心和精髓。
C++面向对象的三大特性为:封装、继承、多态。
对象可以是任何事物,对象有它的属性和行为,例如:
面向人这个对象,它的属性有年龄、身高、体重等,行为有跑、跳、哭、笑等;
面向车这个对象,它的属性有轮胎、汽油、方向盘等,行为有载人、播放音乐、开空调等;
对于具有相同性质的对象,我们概括为类(class),人属于人类,车属于车类。
一、封装
封装的含义:
· 将属性和行为作为一个整体
· 将属性和行为加以权限控制
1.属性和行为
将拥有相同性质的对象归为一个类(class),并将它们的属性和行为归纳。例如:
为人设计一个人类,它的属性(成员)有年龄和名字,行为(函数)有打招呼
class human //设计一个人类
{
public:
int age; //属性:年龄
string name; //属性:姓名
void show() //行为:打招呼
{
cout<<"我的名字是"<<name<<" ,今年"<<age<<endl;
}
};
int main()
{
human qhl; //创建一个人类的对象qhl
qhl.age=18;
qhl.name="qhl"; //为对象qhl的属性赋值
qhl.show(); //qhl作出行为
return 0;
}
2.访问权限
访问权限有三种:public公共权限,private私有权限,protected保护权限
public:类内可以访问,类外也可以访问
private:类内可以访问,类外不可以访问,子类不可以访问父类的内容(之后的继承内容会学到)
protected:类内可以访问,类外不可以访问,但子类可以访问父类的内容
例:创建一个银行卡类
class BankCard
{
public:
string name; //用户名
string cardnumber; //卡号
private:
string password; //密码
protected:
int money; //余额
void set() //该函数编译正确,因为它属于类内,可以访问所有类内所有成员(属性)
{
name = "qhl";
cardnumber = "123456789";
password = "123456";
money = 10000;
}
};
int main()
{
BankCard bankcard; //编译正确,类外可以访问public成员
bankcard.name = "用户名"; //编译正确,类外可以访问public成员
bankcard.cardnumber = "123456789"; //编译正确,类外可以访问public成员
bankcard.password = "123456"; //编译错误,类外不可以访问private成员
bankcard.money = 10000; //编译错误,类外不可以访问protected成员
}
可以对属性进行访问权限设置,来决定其他用户对类内属性的操作权限。
3.struct和class的区别
在C++中struct和class唯一的区别就在于 默认的访问权限不同:
· struct 默认权限为公共
· class 默认权限为私有
例如:
class C
{
int c; //默认该成员私有
};
struct S
{
int s; //默认该成员公共
}
二、对象的初始化和清理
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
1.构造函数和析构函数
C++中利用了构造函数和析构函数解决上述问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。若我们自身不提供构造和析构函数,编译器会自动提供,但编译器提供的构造和析构函数是空实现的。
(1)构造函数
构造函数主要用于创建对象时为对象的成员属性赋值
语法:
类名(){}
a)构造函数没有返回值也不用写void
b)函数名与类名相同
c)构造函数可以有参数,因此可以发生重载
d)程序在调用对象时会自动调用构造函数,无须手动调用,而且只会调用一次
(2)析构函数
析构函数主要作用在于对象销毁前系统自动调用,执行一些清理工作
语法:
~类名(){}
析构函数没有返回值也不写void
函数名与类名相同
析构函数不可以有参数,因此不可以发生重载
程序在对象销毁前会自动调用析构,而且只会调用一次
例:
class Student
{
public:
string name;
int age;
int score;
//构造函数
Student()
{
cout<<"Student构造函数被调用 "<<endl;
}
//析构函数
~Student()
{
cout<<"Student析构函数被调用 "<<endl;;
}
};
int main()
{
Student stu1;
return 0;
}
结果:
结果输出中看不到析构函数的输出是因为析构函数是在整个程序结束后再调用,所以窗口中一般看不到调用析构函数的结果。
C++还提供了初始化列表语法,用来初始化属性:
语法:
类名():属性1(值1),属性2(值2)...{}
传统初始化操作:
//传统初始化操作
Student(int a, int s)
{
int age = a ;
int score = s;
}
初始化列表操作:
Student():age(10),score(20) {}
2.构造函数的分类
(1)无参构造函数
无参构造函数是系统自动提供的,如上述例子
(2)有参构造函数
即有参数的构造函数,是由程序员自己设置的
例:
Student(int a)
{
age = a;
cout<<"Student有参构造函数被调用 "<<endl;
}//有参构造函数
(3)拷贝构造函数
拷贝构造函数的参数是该类的一个对象,也就是说能够复制一个一模一样的对象,拷贝的同时还要以引用的方式传入这个对象
例:
Student( const Student &stu1)
{
age = p.age; //将传入的数据都复制
cout<<"Student拷贝构造函数被调用"<<endl;
} //拷贝构造函数
(4)构造函数的调用
构造函数有三种调用的方法:
a)括号法:
Student stu1; //调用默认无参构造函数
Student stu2(10); //调用有参构造函数
Student stu3(stu2); //调用拷贝构造函数
b)显示法:
Student stu1; //调用无参构造
Student stu2 = Student(10); //调用有参构造
Student stu3 = Student(stu2); //调用拷贝构造
//其中的Student(10)是一种匿名构造
//匿名对象是没有名称的,当该行代码执行结束后会立即回收匿名对象
c)隐式转换法:
Student stu1 = 10; //调用有参构造,相当于写了 Student stu1 = Student(10)
Student stu2 = stu1; //调用拷贝构造
(5)深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
以上拷贝函数的例子和编译器提供的例子都是浅拷贝,接下来提供一个深拷贝的例子:
class Student
{
public:
int *age;
//深拷贝构造函数
Student(int a)
{
age = new int (a);
cout<<"Student深拷贝构造函数被调用 "<<endl;
}
};
int main()
{
Student stu1(10);
Student stu2(stu1);
cout<<"stu2的年龄是:"<<*stu2.age<<endl;
return 0;
}
三、类的成员
1.类对象作为类成员
C++中类的成员可以是另一个类的对象,我们称这个对象为 对象成员
例如:B类中有a作为成员,而a是A类的一个对象
class Person{};
class Student
{
Person a;
}
构造函数的调用顺序是,先调用Person的构造函数,再调用Student的构造函数
2.静态成员
静态成员就是再成员变量和成员函数前加上关键字static,称为静态成员。
(1)静态成员变量:①所有对象共享同一份数据②在编译阶段分配内存③类内声明,类外初始化
例:
class Student
{
public:
static int school_number;//静态成员变量,类内声明
};
int Student::school_number = 10202401; //类外初始化
int main()
{
Student stu1;
cout<<stu1.school_number<<endl; //结果为10202401
Student stu2;
stu2.school_number = 20202402; //修改静态成员变量
cout<<stu2.school_number<<endl; //结果为20202402
cout<<stu1.school_number<<endl; //结果也为20202402,因为它们共用的是一个静态成员
return 0;
}
(2)静态成员函数:①所有对象共享一个函数②静态成员函数只能访问静态成员变量
例:
class Student
{
public:
static int school_number;//静态成员变量,类内声明
static void show()
{
school_number = 20202402; //静态成员函数只能访问静态成员变量
cout<<"学校编号为 "<<school_number<<endl;
}
};
int main()
{
Person::show(); //不需要创建对象也可以访问
Student stu1;
stu1.show(); //也可以通过对象访问
return 0;
}
3.成员的存储和this指针
在C++中,类内的成员变量和成员函数是分开存储的。
只有非静态成员变量占该对象的内存空间,
而成员函数(即静态成员函数与非静态成员函数)不占对象的内存空间。
class Student
{
public:
static int school_number; //静态成员变量不占用对象空间
static void show_shcool() //静态成员函数不占用对象空间
{
cout<<"school_number "<<school_number<<endl;
}
int age; //非静态成员变量占用对象空间
void show_age(){} //非静态成员变量不占用对象空间
};
每一个非静态成员函数智慧诞生一份函数实例,也就是说一个类的多个对象会共用同一块代码。为了解决函数代码能区分是哪一个对象调用自己,C++提供了特殊的对象指针this指针。
this指针指向被调用的成员函数所属的对象
this指针的用途:
···当函数形参和成员变量同名时会产生冲突,此时可用this指针来区分
···在类的非静态成员函数中返回对象本身,可使用return *this4
例:
class Student
{
public:
int age;
Student(int age)
{
this->age = age; //此时对象的成员名称和传入数据的名称区分开来,不会产生冲突
}
Student& show(Student &s)
{
this->age += s.age ;
return *this; //返回一个对象的指针,它可以再调用一次这个函数
}
};
int main()
{
Student stu1(20);
Student stu2(10);
stu2.show(stu1).show(stu1).show(stu1); //可以多次调用这个函数
reurn 0;
}
空指针也是可以访问成员函数的
4.常函数和常对象
常函数:
···成员函数后加const后我们称这个函数为常函数
···常函数内不可以修改成员属性
···成员属性声明时加上关键字mutable后,在常函数中依然可以修改
常对象:
···声明对象前加const称该对象为常对象
···常对象只能调用常函数
例:
class Student
{
public:
string name;
mutable int age; //特殊的可变变量,即使在常函数中也可以修改
int score;
void func() const
{
this->age = 18; //正确,可变成员变量在常函数中也可以修改
this->score = 100; //错误,常函数不能修改一般的成员变量
}
void show(){}
};
int main()
{
const Student stu1; //声明一个常对象
stu1.score = 90 ; //错误,常对象的普通成员变量不能修改
stu1.age = 20 ; //正确,常对象中的可变成员变量可以修改
stu1.func(); //正确,常对象可以调用常函数
stu1.show(); //错误,常对象不可以调用普通成员函数
}
5.友元(friend)
有些私有属性(private)也想要类外的一些函数或类进行访问,这就需要用到友元(friend)
友元的目的就是让一个函数或类 能访问另一个类的私有成员,它的关键字为friend。
声明全局函数作友元:
void goodgay(Student &stu){}; //全局函数
class Student
{
friend void goodgay(Student &stu); //此时goodgay这个全局函数就可以访问Student类私有成员
private:
int password;
}; //学生类
类作友元:
class Student;
class GoodGay {
friend class Student; // 声明Student为GoodGay的友元类
};
成员函数作友元:
class Student
{
friend void GoodGay::func(); //声明GoodGay类的成员函数func为友元
}
class GoodGay
{
public:
void func(){};
};
五、运算符重载
运算符重载:对已有的运算符进行重新定义,赋予它另一种功能,用来适应不同的数据类型
关键字:operator
1.加号运算符重载(+)
对于整型和浮点型,进行原加号+运算,编译器知道如何运算,但对于其他数据类型却不知道如何运算。
加号重载作用:实现两个自定义数据类型相加的运算
例:
class Student
{
public:
int score;
int money;
Student(int s,int m)
{
score = s;
money = m;
}
};
Student operator+(Student s1,Student s2) //重载运算符+
{
Student s3(s1.score + s2.score,s1.money + s2.money);
return s3;
}
int main()
{
Student stu1(80 , 900); Student stu2(90 , 1000) ;
Student stu3 = stu1 + stu2;
cout << "stu3.score = " << stu3.score << endl; //输出170
cout<<"stu3.money = " << stu3.money << endl; //输出1900
return 0;
}
通过重载运算符+,就可以通过+来实现两个类对象的相加。
2.左移运算符重载(<<)
作用:可以输出自定义数据类型
例:
ostream & operator<<(ostream &out, Student &s) //重载<<运算符
{
cout<<"分数:"<<s.score<<",钱:"<<s.money;
}
int main()
{
Student stu1(100,1000);
cout<<stu1<<endl;
return 0;
}
如此我们就可以输出Student类的对象。
其中ostream
代表“输出流”,用于将数据输出到控制台、文件等。
3.函数调用运算符重载( () )
函数调用运算符()也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活(在后续STL的学习会用得比较多)
例:
class Print
{
public:
int a,b;
void operator()(int a,int b)
{
cout<<a + b<<endl;
}
};
int main()
{
Print myprint;
int a=1,b=2;
myprint(a,b); //结果输出为3
return 0;
}
如此,我们实现了函数调用的同时输出a,b两数的和。
除了以上运算符重载,还有关系运算符重载< >(用于两个自定义类型对象进行对比)、赋值运算符重载=(用于两个自定义类型对象之间进行赋值操作)等等。