1. 类和对象
1. 类
- 类是构成对象的一个蓝图
- 可以拥于属性(用于表示数据)
- 可以拥有方法
- 可以隐藏数据和方法
- 可以对外提供公开的接口
class Student{
string name; // 姓名
int age; //年龄
};
//实际上前面大家见过的Vector 和 string 都是类
2. 对象
类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样
- 在栈中创建对象
,用这种方法创建的对象,内存分配到栈里(Stack)。 直接使用
.
来访问成员。当程序对象所在的代码块结束运行(方法运行结束),对象会自动删除,内存自动释放。
Student student1 ;
Student student2 ;
- 在堆中创建对象
这种方法创建的对象,内存分配到堆里(Heap)。一般使用
*
或者->
调用对象的方法。箭头操作符”->"将解引用(dereferencing*)和成员使用(member access.)结合起来,
//在堆中创建对象,这种方式创建的对象,数据会存放在堆中
//new 关键字是在堆中申请动态内存,所以这里不能写成 s3 而应该是指针。
//这里相当于是申请了一块堆内存用于存放Student对象的数据
Student *s3 = new Student;
2. 访问修饰符
python中修饰成员访问权限,采用的是下划线的形态来修饰。
c++ 对于成员的访问有三种访问操作符
public
|prive
|protected
, 默认情况下是private
public
: 公开权限,任何位置都可以访问
private
: 私有权限,只能自己内部访问及其友元函数
protected
: 类内、子类及友元函数可访问
class Student{
private: //表示下面的成员为私有
string name; // 姓名
int age; //年龄
public: //表示下面的函数为公开
void run(){}
};
3. 访问类中的成员
一旦创建出来了某个类的对象,那么可以使用
.
操作符来访问类中的成员。
- 使用对象直接访问成员
//对象
Student student
//获取成员
student.name;
student.read("哈利波特")
- 使用指针访问成员
如果我们拥有了某个对象指针(也就是知道了某个对象的内存地址),那么可以使用dereference操作符(也就是*)来访问成员
- 使用解引用访问
可以使用
*
解引用,获取到对象后,再访问成员
Student *s3 = new Student; //创建一个学生对象, 不过*s3指向的是学生对象位于的内存地址
//上面的s3 表示的是一个学生指针 ,这里在s3前面在家* ,由于s3已经就是指针了,所以在指针的基础上再加*表示获取到该指针指向的那块内存地址存放的数据。也就是Student对象。
(*s3).name = "张三feng";
(*s3).read("飞鸟集");
delete s3; //释放内存
- 使用指针访问
使用指针,配合
->
可以直接访问指向的对象成员
Student *s3 = new Student;
s3->name = "张三";
s3->read("哈利波特");
delete s3; //释放内存
4. 实现成员函数
- 成员函数可以在类中声明时直接实现,也可以在类的外部。
- 可以在类的外部实现成员你函数 , 需要使用 类名::函数名
- 可以使声明和实现分开, 声明放在头文件中 (.h) , 实现就放在cpp文件中
- 直接在类中实现 或者在 外部实现
class Student{
private :
int age ;
stringname;
public :
void read(string bookname){
cout<< bookname << endl;
}
void speak(string something);
}
void Student::speak(string something){
cout << "说说说---" << something << endl;
}
- 分离声明和实现
声明放到 头文件中,实现放到cpp文件中 。 头文件的声明,需要在前后包裹一段特殊的样式代码。这段代码确保了一旦该头文件多次包含执行时,出现重复定义的错误。
当第一次包含Student.h时,由于没有定义STUDENT_H,条件为真,这样就会包含(执行)#ifndef STUDENT_H和#endif之间的代码,当第二次包含Student.h时前面一次已经定义了STUDENT_H,条件为假,#ifndef_STUDENT_H_和#endif之间的代码也就不会再次被包含,这样就避免了重定义了。
Student.h
//后面的大写表示标识,可以随意命名。不过一般会合头文件有关联性
//标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
class Student{
private :
int age ;
stringname;
public :
void read(string bookname);
void speak(string something);
}
#endif
在cpp文件中实现
main.cpp
#include "Student.h"
using namespace std;
void Student:read(string bookname){
cout << "看书:"<
5. 特殊成员函数
当定义一个类后,它的对象在未来的操作中,总会不可避免的总会碰到如下的行为:
创建
、拷贝
、赋值
、移动
、销毁
。 这些操作实际上是通过六种特殊的成员函数来控制的:构造函数
、析构函数
拷贝构造函数
、拷贝赋值函数
、移动构造函数
、移动赋值函数
。默认情况下,编译器会为新创建的类添加这些函数,以便它的对象在未来能够执行这些操作。
1. 构造函数
构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。与类名同名,没有返回值,可以被重载,通常用来做初始化工作。 在python中,有类似的 ____init___ 函数用于初始化工作
class Student{
string name;
int age ;
public :
//构造函数
Student();
Student(string name );
Student(string name , int age );
};
//创建三个对象,会执行三个对应你的构造函数
Student s1 ;
Student s1{"张三"};
Student s1{"张三",28};
1. 构造函数初始化列表
前面我们队成员的初始化工作,都是在构造函数的函数体里面完成的。如果使用初始化列表,那么成员的初始化赋值是在函数体执行前完成,并且初始化列表有一个优点是:
防止类型收窄
,换句话说就是精度丢失
有三种情况需要使用到构造函数初始化列表 //TODO
- 情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
- 情况二、需要初始化const修饰的类成员或初始化引用成员数据;
- 情况三、子类初始化父类的私有成员;
//早期的方式
Student(){
name = "张三";
age = 19;
}
//更好的方式
Student():name{"李四"},age{19}{
}
2. 委托构造函数
一般来说,如果给类提供了多个构造函数,那么代码的重复性会比较高,有些构造函数可能需要包含其他构造函数中已有的代码,为了让编码工作更简单,C++11 允许程序员在一个构造函数的定义中使用另一个构造函数。这种现象被称为
委托
.
- 早前的构造函数写法
class Student{
string name;
int age ;
public:
Student():name{"张三"},age{19}{}; //无参构造
Student(string name_val):name{name_val},age{19}{}; //一个参数构造
Student(string name_val , int age_val ):name{name_val},age{age_val}{}; //两个参数
};
- 委托构造函数写法:
委托构造函数实际上就是已经有一个构造函数定义好了所有初始化工作的逻辑,那么剩下的构造函数就不用做这个活了,全部交给它来做即可。有点类似,A调用C, B调用C ,而C 把所有的初始化代码都写完了。那么A和B就不用写了。
class Student{
string name;
int age ;
public:
Student():Student{"张三" , 19}{}; //无参构造
Student(string name_val):Student{name_val , 19}{}; //一个参数构造
Student(string name_val , int age_val):name{name_val},age{age_val}{}; //两个参数
};
2. 析构函数
和python一样,c++也有析构函数。类的析构函数是类的一种特殊的成员函数,与构造函数正好相反,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。不能被重载,一般用于释放资源。
class Student{
string name;
int age ;
public :
//构造函数
Student(){}
Student(string name ){}
Student(string name , int age ){}
//析构函{}
~Student(){}
};
Student *s = new Student(); //创建对象,执行构造函数
delete s; //删除空间,执行析构函数
3. 拷贝构造函数
- 引子
C++中经常使用一个常量或变量初始化另一个变量,例如:
int a = 3;
int b = a;
使用类创建对象时,构造函数被自动调用以完成对象的初始化,那么能否象简单变量的初始化一样,直接用一个对象来初始化另一个对象呢?
答案是肯定的,以Student类为例:
Student s1{"张三" , 19 };
Student s2 = s1;
不难看出,s2对象中的成员数据和 s1 是一样的。相当于将s1中每个数据成员的值复制到s2中,这是表面现象。实际上,系统调用了一个拷贝构造函数。如果类定义中没有显式定义该复制构造函数时,编译器会隐式定义一个缺省的复制构造函数,其原型形式为: 类名::类名(const 类名 &) 如:
point:: point (const point &);
Student(const Student &s)
1. 定义
如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,则此构造函数为拷贝构造函数。 拷贝构造函数的第一个参数必须是引用类型。
class Student{
public:
Student(){} //默认构造函数
Student(const Student &s){}; //拷贝构造函数 因为一般不会修改外部传递进来的对象,所以拷贝构造的参数都是const
};
- 初始化
Student s1 ("张三" , 18); // 执行构造
Student s2 = s1; // s2的创建会执行拷贝构造,
- 函数参数
int main(){
Student stu("张三",18);
printStudent(stu)
}
//函数参数会执行一次拷贝,默认执行值传递,所以会执行拷贝
void printStudent(Student stu){
cout << stu.name << endl;
}
- 函数返回值
先拷贝给临时对象返回,当函数执行完毕后,内部生成的对象就会销毁,执行析构函数。
//临时对象的产生,会执行一次拷贝构造。
Student createStudent(String name , int age){
//执行一次构造函数
return Student(name ,age)
}
- 编译器优化
默认情况下,编译器对对象的创建工作有进行优化,如果一个对象创建出来,仅仅是当做一个临时对象去初始化另一个对象,那么编译器会自动省略掉这个临时对象的创建,也就是拷贝构造不会执行。可以在编译的时候,添加参数
-fno-elide-constructors
来禁止这个优化。那么编译器提供了这个优化,是否表示在以后编码就可以忽略拷贝的隐患呢? 网上有这样的一个问题。在StackOverFlow上有一个问题,为什么&什么时候使用
-fno-elide-constructors
参数 。
HasPtrMem getHpm(){
HasPtrMem h = HasPtrMem();
return h ;
}
int main() {
HasPtrMem h = getHpm();
return 0;
}
2. 浅拷贝
的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。如果数据中有属于
动态成员
( 在堆内存存放 ) ,那么浅拷贝只是做指向而已,不会开辟新的空间。
class Student {
public:
int age ;
string name;
public :
//构造函数
Student(){
age = 19 ;
name = "张三";
cout<< " 调用了 构造函数" << endl;
}
//拷贝构造函数
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
}
};
3. 深拷贝
深拷贝
也是执行拷贝,只是在面对对象含有动态成员
时,会执行新内存的开辟,而不是作简单的指向。在发生对象拷贝的情况下,如果对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配。
class Student {
public:
int age ;
string name;
string *address;
// 这里还是默认的浅拷贝。 由于address是指针类型,如果是浅拷贝,那么两个指针会指向同一个位置。
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
address = s.address;
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
//这里将会删除两次内存空间
delete address;
address = nullptr;
}
}
为了避免执行指向同一块内存空间,我们需要给新对象的address开辟新的内存。 这其实就是和浅拷贝的不同。
Student(const Student & s){
cout << "调用了拷贝构造函数" << endl;
age = s.age;
name = s.name;
if(address == nullptr){
//开辟新的空间
address = new string();
*address = s.address;
}
}
//析构函数
~Student(){
cout << "调用了析构函数" << endl;
if(address != nullptr){
delete address;
address = nullptr;
}
}
4. 触发拷贝的场景
如果是生成临时性对象或者是使用原有对象来初始化现在的对象,那么都会执行拷贝构造。 编译器有时候为了避免拷贝生成临时对象而消耗内存空间,所以默认会有优化、避免掉一些情况。
一般调用拷贝构造函数的场景有以下几个:
- 对象的创建依赖于其他对象。
- 函数参数
- 函数返回值
默认编译器会优化,所以打印的日志可能不是我们所期望的,这时候,如果手动编译的话,可以添加参数,
-fno-elide-constructors
如果使用cmake编译,可以添加配置SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")
g++ -std=c++11 main.cpp -fno-elide-constructors
- 对象的创建依赖于其他对象
Student stu1("张三",18); //执行构造函数
Student stu2 = stu1; //执行拷贝构造函数
- 函数参数
编译器不会优化这个方向,因为一旦优化掉了,那么就不会执行拷贝的工作,那就代表传递进来的对象实际上就是外部的原对象,有可能在函数内部对对象进行了修改,会导致外部对象跟着修改。所以默认情况下,只要是函数参数传递,都会发生拷贝。 如果不想发生拷贝,请使用 引用 或者 指针。
void printStun(Student student){
}
int main(){
Student stu1("张三",18); //执行构造函数
printStudent(stu1); //执行拷贝构造函数
}
- 函数返回值
为了避免过多的临时性对象创建,消耗内存,编译器内部做了优化,函数的返回值不会再产生临时对象,直接把生成的对象赋值给外面接收的变量
//构造函数 由于函数执行完毕会有个临时对象来接收值,所以这里还会执行拷贝构造
Student createStu(){
return Student("张三",18);
}
int main(){
Student stu = createStu(); //这里还会执行构造函数
return 0;
}
5. 再说函数返回值
c++ 严禁函数返回内部的存放于栈内存的局部变量引用或者指针,因为函数一旦执行完毕,那么内部的所有空间都将会被释放,这时如果在外面操作返回值,将出现错误。所以有时候临时拷贝的工作就变得不可避免。
int* getNum(){
int a = 9;
return &a ;
}
int* getNum2(){
int a = new int(9);
return &a ;
}
int main(){
int *p = getNum(); //错误! 一旦getNum()执行完毕,内部栈空间都将释放掉。
int *p = getNum2(); // 正确,因为函数返回的指针指向的地址位于堆内存中。
return 0 ;
}
4. 移动构造
有时候我们需要新创建的对象,拥有旧对象一模一样的数据,当然这可以使用拷贝操作来完成。但是假设旧对象不再使用,那么拷贝操作就显得有点弱,因为旧对象仍然占据着空间。C++11中推出了 移动构造操作,也就是完全的把旧对象的数据 "移动" 到新对象里面去,并且把旧对象被清空了。
注意: 移动构造或者是拷贝构造,针对的都是数据的拷贝或者移动,对象的该创建还是创建,他们两针对的仅仅是数据是以和种方式得来而已。
- 移动语义
要想完成移动构造,需要配合右值引用来实现。因为把某一份数据,移动到另一个地方去,实际上操作的是右值,而右值引用恰好指向的是右值。这常常出现在对象的搬运上,假设现在要做两份数据的交换,最初的设想是进行值的拷贝、复制,但是这样会产生一份临时拷贝值,不如直接移动过去划算。但在C++里面的
移动
实际上并不是真的在移动数据,只是窃取了原来数据的控制权而已。
1. 没有移动构造
使用getStu函数生成一个学生对象,调用返回后,使用stu来接收。 在没有移动构造函数的情况下,临时对象的产生会调用拷贝构造。此举会造成资源的浪费,并且效率也低。编译器为了避免过多的临时对象的创建工作,内部已经做了优化。
- 学生类
class Student{
public :
int *age;
Student():age(new int(18)){
cout << "执行构造函数~!~"<
- main函数
int main(){
Student stu = getStu();
return 0 ;
}
2. 使用移动构造
使用移动构造,会使得在返回对象时,不会调用拷贝构造函数,这也就避免产生了对象的拷贝工作。
- 学生类
class Student{
public :
int *age;
Student():age(new int(18)){
cout << "执行构造函数~!~"<
- main
int main(){
Student stu = getStu();
return 0 ;
}
3. std::move函数
move
函数名字很具有迷惑性,但是它并不能移动任何东西。它唯一的作用就是把一个左值转化成一个对应的右值引用类型,继而可以通过右值引用使用该值。并且在使用move函数可以避免不必要的拷贝工作,move
是将对象的状态或者所有权从一个对象转化到另一个对象,只是转换状态或者控制权,没有内存的搬迁和拷贝 . 使用move函数即表示该对象已经不再使用,要被即将销毁了。
int a = 3;
int &b = a ; //左值引用指向左值
int &&c = 3; //右值引用指向右值
int &&d = move(b); //右值引用,指向右值, move函数强制转化 左值引用b 成右值引用
- 使用move出发移动构造函数
默认情况下,如果直接赋值,那么执行的是拷贝构造函数,并且有时候为了避免频繁的执行拷贝工作,可以直接使用
move
函数转化左值成右值引用,进而变成直接操作数据。
Student stu1 ;
Student stu2 = stu1; // 执行拷贝构造函数
Student stu3 = move(stu1); // 执行移动构造函数
Student stu3(movestu1) ; // 和上一行代码同效果
11. this 指针
1. 引入
在早前的编码中,我们都会刻意的避免的函数的参数名称和成员变量名称一致,这是为了让我们更方便的赋值。其实他们的名称也可以一样,只是要多费点功夫而已。如下:
class Student{
int age ;
string name;
Student(int age_val , string name_val){
//默认情况下,前面的两个变量有编译器加上的this指针
age = age_val ;
name = name_val ;
}
Student(int age , string name){
//默认情况下,下面两行代码并没有完成赋值工作。
age = age ;
name = name ;
//=============================
//前面加上this -> 即可完成操作。
this-> age = age ;
this-> name = name ;
}
};
2. 含义
this
只能在成员函数中使用。全局函数,静态函数都不能使用this
。全局函数不属于任何一个类,静态函数也不依赖任何一个类,所以他们都不会有this指针
。 在外部看来,每一个对象都拥有自己的成员函数,一般情况下不写this,而是让系统进行默认配置。this指针永远指向当前对象 , 所以成员函数中的,通过this即可知道操作的是哪个对象的数据。
- this指针的作用有两个
- 处理同名变量问题
- 返回对象本身
class Student{
int age ;
string name;
Student(int age , string name){
//默认情况下,下面两行代码并没有完成赋值工作。
age = age ;
name = name ;
//=============================
//前面加上this -> 即可完成操作。
this-> age = age ;
this-> name = name ;
}
//获取对象本身。注意: 这里的返回值不要写成Student,否则返回的是一个临时的拷贝对象
Student& getStu(){
return *this; //this表示指针, 在指针前面加上* 表示解引用。根据指针获取到指向的对象
}
};
Student s1 (18 , "张三");
Student &s2 = s1.getStu(); //使用引用来接收,表示指向同一个引用,
12. 常函数
在编写代码的时候,如果确定了某个成员函数不会修改成员变量,那么可以把该函数声明为常函数。常函数其实也是函数,它浓缩了函数和
常
这个概念在里面,函数体内不允许修改成员变量,除非该变量使用mutable
修饰.常函数的const修饰的是this指针,在常函数中的this形同:
const 类名
, 表示指向常量的指针。 所以也就解释了为什么在常函数中无法修改成员变量。
语法:
void fun() const {}
示例 :
class Student{
public:
string name ;
int age ;
public :
//在此添加 const 即可变成常函数,
void printStudent() const{
cout << name << " " << age << endl;
}
};
13. 常对象
常对象其实也就是一个变量,和以前的常量没有相差多少。不同的是如今对象里面可以能包含很多的变量,以前的常量指的只是一个变量而已。 若类中有变量使用
mutable
修饰,那么该变量在常对象状态下,可以被修改
const Student s1 ("zhangsan " ,18);
cout << s1.name << endl; //访问常对象中的成员 ,OK
s1.name = "李四" ; // 试图修改常对象中的成员 , error
- 注意:
-
- 无法修改常对象中的成员(变量)
- 除非某个成员使用
mutable
修饰 - 常函数函不能访问普通的成员函数,但是可以访问常函数
- 普通的对象可以访问成原函数,也可以访问常函数
-
14. 静态成员
有时候需要不管创建多少次对象,某些变量的值都是一样的,那么此时可以把该变量变成静态修饰(static).
静态成员被类的所有对象所共享,成员不能在类中定义的时候初始化,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化
- 静态成员变量
假设现在要记录来参加当年毕业的学生信息,无论他们的身份迥异,都有一个共有的属性,就是同一个学校。如果不对该属性进行修饰,那么100个学生对象就要开辟100个位置来存储同一个school数据,但是其实可以使用static修饰,只需要一份空间即可。
class Student{
public:
int age ;
string name;
static string school; //静态成员
};
Student::school = "北京大学";
- 静态成员函数
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
class Student{
public:
int age ;
string name;
static string school; //静态成员
public :
static string getSchool(){
//不能访问 name 和 age
return school;
}
};
- 注意:
- 静态成员属于类,不属于对象
- 静态成员变量必须在类中声明,在类的外部初始化
- 静态成员变量的声明周期是整个程序,作用域在类的内部
- 静态成员函数只能访问静态成员变量
- 静态成员可以使用类来访问,也可以使用对象来访问。
15. 结构体和类
结构体是一个由程序员定义的数据类型,可以容纳许多不同的数据值。在过去,面向对象编程的应用尚未普及之前,程序员通常使用这些从逻辑上连接在一起的数据组合到一个单元中。一旦结构体类型被声明并且其数据成员被标识,即可创建该类型的多个变量,就像可以为同一个类创建多个对象一样。 结构体是由C语言发明出来的,它和类其实没有差多少,只是它的所有成员默认都是public公开。
声明结构体的方式和声明类的方式大致相同,其区别如下:
- 使用关键字 struct 而不是关键字 class。
- 尽管结构体可以包含成员函数,但它们很少这样做。所以,通常情况下结构体声明只会声明成员变量。
- 结构体声明通常不包括 public 或 private 的访问修饰符, 因为它的所有成员默认都是public
- 类成员默认情况是私有的,而结构体的成员则默认为 public。程序员通常希望它们保持公开,只需使用默认值即可。
struct Student{
string name;
int age;
void run(){
}
}
Student s1 ;
s1.name = "zhangsan";
s1.age = 18 ;
16. 友元类
- 概念理解
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。
在类的外部无法访问类的私有成员,但是有时候需要它,那么可以借助友元函数 来实现。
友元函数
是一种特权函数,C++允许它访问私有成员。程序员可以把一个全局函数、成员函数、甚至整个类声明为友元
友元可以看成是现实生活中的 好闺蜜 或者是 好基友
- 友元函数
友元函数可以直接访问类中的私有成员
class Car{
private:
string color {"红色"}
friend void showColor(Car c);
};
//实现友元函数,函数内部可以直接访问私有成员
void showColor(Car c) {
cout << c.color << endl;
}
- 友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员
class Car{
private:
string color {"红色"}
friend class SSSS; //声明 4S 为友元类
};
class SSSS{
public:
void modifyCar(){ //改装汽车
Car myCar;
myCar.color = "黑色";
}
};