【C++快速入门】面向对象篇
面向对象
- 类与对象
- 对象的内存布局
- *this 与 指针访问对象成员的本质
- 封装性
- 内存空间的布局
- 堆空间
- memset与堆内存的初始化
- 对象的内存(数据段、栈空间、堆空间)
- 构造函数(Constructor)
- 析构函数(Destructor)
- 命名空间(嵌套、合并)
- 继承性
- 成员访问权限(public、protected、private)
- 初始化列表
- 父类的构造函数
- 父类指针、子类指针
- 多态性
- 虚函数的实现原理:虚表
- 调用父类的成员函数
- 虚析构函数
- 纯虚函数
- 多继承(了解,不建议使用)
- 静态成员(static)
- const 成员
- 引用成员类型
- 拷贝构造函数
- 深拷贝、浅拷贝
- 对象型参数和返回值
- 匿名对象(临时对象)
- 隐式构造
- 友元函数、友元类
- 内部类
- 局部类
【C++快速入门】基础语法篇
C++命名规范
每个人都可以有自己的编程规范,没有统一的标准,没有标准答案,没有最好的编程规范
变量名规范参考
- 全局变量:
g_xxx
- 成员变量:
m_xxx
- 静态变量:
s_xxx
- 常量:
c_xxx
- 使用驼峰标识:
userName
声明和实现分离
- 函数的声明可以写在
.h
文件中- 函数的实现必须写在
.cpp
文件中
类与对象
C++ 中可以使用 struct
、class
来定义一个类,两者的的区别:
struct
的默认成员权限是public
class
的默认成员权限是private
下面两段代码完全等价:
// 类的定义
// class 的默认成员权限是 private
class Person {public:int m_age; // 成员变量(属性)void run() { // 成员函数(方法)cout << "Person::run() - " << m_age << endl;}
};
// 类的定义
// struct 的默认成员权限是 public
struct Person {int m_age; // 成员变量(属性)void run() { // 成员函数(方法)cout << "Person::run() - " << m_age << endl;}
};
在讲到成员权限之前,我们都用 struct
来定义类,默认属性为 public
较为方便,实际上 class
和 struct
除了这一点真的没有任何区别了。
定义了类以后,如何创建对象呢?
struct Person {int m_age;void run() {cout << "Person::run() - " << m_age << endl;}
};int main(){// 利用类创建对象Person person;person.m_age = 20;person.run();// 通过指针间接访问person对象Person *p = &person;p->m_age = 40;p->run();return 0;
}
上面代码中 person 对象、p指针的内存都是在函数的栈空间,自动分配和回收
- 可以尝试反汇编
struct
和class
,看看是否有其他区别(并没有) - 实际开发中,一般都用
class
表示类,这里是为了演示用struct
对象的内存布局
如下定义的这个类,它的对象的内存布局是什么样子的呢?
struct Person {int m_id;int m_age;int m_height;void display() {cout << "id = " << m_id << ", age = " << m_age<< ", height = " << m_height << endl;}
};
person对象的内存布局如上图所示:对象的内存地址就是对象的第一个成员变量的内存地址。
- 可以理解为
&person == $person.m_id
。
而定义的 3 个成员变量都是 int 型的(x86架构下int型占4个字节),由图可见,m_id
、m_age
、m_height
之间分别差了4个字节(请忽略内存地址的具体值,每次运行效果可能不一样,只要知道成员变量的地址之间差了4个字节即可)。
下面用代码来验证一下:
#include <iostream>
using namespace std;struct Person {int m_id;int m_age;int m_height;void display() {cout << "id = " << m_id << ", age = " << m_age<< ", height = " << m_height << endl;}
};// Person person; // 全局区(数据段)
int main() {// 这个person对象内存在栈空间Person person;person.m_id = 1;person.m_age = 2;person.m_height = 3;cout << "&person \t\t== " << &person << endl; // 内存对象的地址cout << "&person.m_id \t\t== " << &person.m_id << endl; // 对象的第一个成员变量的地址cout << "&person.m_age \t\t== " << &person.m_age << endl;cout << "&person.m_height \t== " << &person.m_height << endl;getchar();return 0;
}
&person == 0x61feb4
&person.m_id == 0x61feb4
&person.m_age == 0x61feb8
&person.m_height == 0x61febc
注意看,【person 对象的地址】 和 【第一个成员变量的地址】是相等的;
其他成员变量之间相差的是4字节,与上面的结论完全符合。
*this 与 指针访问对象成员的本质
this
是指向当前对象的指针
- 对象在调用成员函数的时候,会自动传入当前对象的内存地址
#include<iostream>
using namespace std;struct Person {int m_id;int m_age;int m_height;void display() {cout << "m_id is " << this->m_id << endl;cout << "m_age is " << this->m_age << endl;cout << "m_height is " << this->m_height << endl;}
};
可以利用this.m_age来访问成员变量么?
- 不可以,因为 this 是指针,必须用 this->m_age
思考一下,下面这段代码的执行结果是什么?
#include<iostream>
using namespace std;struct Person {int m_id; // 占peron对象中1~4个字节内存数据int m_age; // 占person对象中5~8字节内存数据int m_height; // 占person对象中9~12字节内存数据void display() {cout << "m_id is " << this->m_id << endl;cout << "m_age is " << this->m_age << endl;cout << "m_height is " << this->m_height << endl;}
};int main(){Person person; // 定义一个对象personperson.m_id = 10;person.m_age = 20;person.m_height = 30;// 实际开发中绝对不会像下面这么写,除非你想被打死Person *p = (Person*)&person.m_age; p->m_id = 40; // 本质是什么p->m_age = 50; person.display(); // 10 40 50cout << "---------------" << endl;p->display(); // 40 50 ccreturn 0;
}
m_id is 10
m_age is 40
m_height is 50
---------------
m_id is 40
m_age is 50
m_height is 6422196
Person *p = (Person*)&person.m_age;
这句代码很奇葩,开发中不会这么写,但是理解它有利于深入理解对象的内存布局以及指针。
很明显,指针变量 p 指向了person
对象的m_age
成员变量(不强转会报错),前面讲的内存布局还记得吗:【person 对象的地址】 和 【第一个成员变量的地址】是相等的,也就是说,指针变量 p 原本指向的地址其实就是person.m_id
的地址,现在我们强行让 p 指向了person.m_age
的地址,想一想m_id
和m_age
的地址之间有什么关系,差了4个字节,原本该指向person.m_id
的 p,现现指针后移了4个字节。
知道了 p 的指向,再看看 p 做了什么,p->m_id = 40; p->m_age = 50;
, 这句话其实是将 p 前4个字节的内存数据修改为40,将 p 第5~8个字节的内存数据修改为50,由于此时的 p 指向的是 &p_age,因此 p 的前4个字节的内存数据实际上就是person.m_age
的数据,第5~8个字节的内存数据实际上是person.m_height
的数据。所以 person
对象的m_age
变为了50,person
对象的m_height
变为50。
void display() {cout << "m_id is " << this->m_id << endl;cout << "m_age is " << this->m_age << endl;cout << "m_height is " << this->m_height << endl;}
还记得这句话吗:对象在调用成员函数的时候,会自动传入当前对象的内存地址,this->m_id
实际上是调用该函数的对象的地址的前4个字节内存数据,this->m_age
是调用该函数的对象的地址的 5~8 个字节的内存数据,this->m_height
是调用该函数的的对象的地址的 9~12 个字节的内存数据。
person.display()
传入的是 person 的地址(person.m_id的地址),打印 person.m_id 地址后面12个字节对应的内存数据则是,10,40,50。
那么调用 p->display()
有什么不同呢?由于 p 指向的是 &person.m_age
,传入的是person.m_age
的地址,打印&person.m_age
地址后面12个字节对应的内存数据是:40,50,垃圾值。
封装性
成员变量私有化,提供公共的getter
和setter
给外界去访问成员变量
#include <iostream>
using namespace std;struct Person {private:int m_age;
public:void setAge(int age) { // 提供一个修改数据的方法if (age <= 0) { // 但是要满足我设定的条件才可以修改m_age = 1; } else {m_age = age;}}int getAge() {return m_age;}
};int main() {Person person;person.setAge(-4);cout << person.getAge() << endl;getchar();return 0;
}
内存空间的布局
每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域:
- 代码段(代码区):用于存放代码
- 数据段(全局区):用于存放全局变量等
- 栈空间:自动分配和回收内存
每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间 - 堆空间:需要主动去申请和释放
堆空间
在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
堆空间的 申请 \ 释放:
malloc
\free
new
\delete
new []
\delete []
注意:
- 申请堆空间成功后,会返回那一段内存空间的地址
- 只要申请了堆空间,最后必须释放,不然可能会存在内存泄露
现在很多高级语言不需要主动管理内存(比如Java),屏蔽了很多内存细节:
- 利:提高开发效率,避免内存使用不当或泄露
- 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
x86环境中的内存分配
指针变量p 处于栈空间;而通过malloc
分配的内存,即 p指向的地址的值 处于堆空间。
使用maolloc
分配空间:
//int *p = (int *) malloc(4);
// *p = 10;// 使用malloc必须写清楚分配空间大小,一般用 sizeof 计算
char *p = (char *) malloc(4);
p[0] = 10; // *p = 10;
p[1] = 11; // *(p + 1) = 11;
p[2] = 12; // *(p + 2) = 12;
p[3] = 13; // *(p + 3) = 13;free(p); // 不要忘记 free
使用new
初始化变量:
int *p = new int; // 使用new初始化变量
*p = 10;
delete p;
使用new
初始化数组
char *p = new char[4]; // 使用new初始化数组
delete[] p;
memset与堆内存的初始化
memset
函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法
- 参数1:目标地址
- 参数2:要设置的值
- 参数3:设置的长度
#include <iostream>
using namespace std;struct Person {int m_id; int m_age; int m_height;
};
int main(){Person person;person.m_id = 1;person.m_age = 20;person.m_height = 180;// 地址为 &person// 设置值为 0// 全部元素memset(&person, 0, sizeof(person));// m_id=0, m_age=0, m_height=0;Person persons[] = {{1, 20, 180}, {2, 25, 165}, {3, 27, 170}};// 地址为 perons// 设置值为 0// 全部元素memset(persons, 0, sizeof(persons));
}
堆空间的初始化:
#include <iostream>
using namespace std;int main(){int *p1 = (int*)malloc(sizeof(int)); // p1未初始化int *p2 = (int*)malloc(sizeof(int));memset(p2, 0, sizeof(int)); // 将*p2的每一个字节初始化为0
}
#include <iostream>
using namespace std;int main(){int *p1 = new int; // 为被初始化int *p2 = new int(); // 被初始化为0int *p3 = new int(5); // 被初始化为5int *p4 = new int[3]; // 数组元素未被初始化int *p5 = new int[3](); // 3个数组元素都被初始化为0int *p6 = new int[3]{}; // 3个数组元素都被初始化为0int *p7 = new int[3]{5}; //数组首元素被初始化为5,其他元素被初始化为5
}
对象的内存(数据段、栈空间、堆空间)
对象的内存可以存在于 3 种地方:
- 全局区(数据段):全局变量
- 栈空间:函数里面的局部变量
- 堆空间:动态申请内存(
malloc
、new
等)
// 全局区
Person g_person;int main(){// Peron 创建的对象在栈空间Person person;// 指针变量p依旧在栈空间// Person 创建的对象在堆空间Person *p = new Person();return 0;
}
构造函数(Constructor)
构造函数(构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作,特点是:
- 函数名与类同名,无返回值(
void
都不能写),可以有参数,可以重载,可以有多个构造函数 - 一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象
注意:通过malloc
分配的对象不会调用构造函数
一个广为流传的、很多教程\书籍都推崇的错误结论:**默认情况下,编译器会为每一个类生成空的无参的构造函数
正确的理解:在某些特定的情况下,编译器才会为类生成空的无参的构造函数
创建对象的时候会调用构造函数:
#include <iostream>
using namespace std;struct Person {int m_age;Person() {cout << "Person::Person()" << endl;}
};int main() {Person *p = new Person; // 会调用构造函数delete p;
}
通过malloc
分配的对象不会调用构造函数:
#include <iostream>
using namespace std;
struct Person {int m_age;Person() {cout << "Person::Person()" << endl;}
};
int main() {Person *p = (Person *) malloc(sizeof(Person)); // 不会调用构造函数p->m_age = 10;free(p);return 0;
}
看看下面这段代码会调用几次构造函数?
#include <iostream>
using namespace std;
struct Person {int m_age;Person() {m_age = 0;cout << "Person()" << endl;}Person(int age) {m_age = age;cout << "Person(int age)" << endl;}
};
// 全局区
Person g_person0; // Person() ---1
Person g_person1(); // 函数声明 ---不会调用构造函数
Person g_person2(10); // Person(int) ---2
int main() {// 栈空间Person person0; // Person() ---3Person person1(); // 函数声明 ---不会调用构造函数 Person person2(20); // Person(int) ---4// 堆空间Person *p0 = new Person; // Person()---5Person *p1 = new Person(); // Person()---6Person *p2 = new Person(30); // Person(int)---7/* 4个无参,3个有参,一共创建了7个Person对象 */getchar();return 0;
}
如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化。
成员变量的初始化:
#include <iostream>
using namespace std;struct Person {int m_age;
};// 全局区:成员变量初始化为0
Person g_person;
int main() {// 栈空间:没有初始化成员变量Person person; // 堆空间:没有初始化成员变量Person *p0 = new Person;// 堆空间:成员变量初始化为0Person *p1 = new Person();// 堆空间:成员变量初始化为0Person *p = new Person[3] {};cout << g_person.m_age << endl; // 0cout << person.m_age << endl; // 垃圾数cout << p0->m_age << endl; // 垃圾数cout << p1->m_age << endl; // 0cout << p[0].m_age << endl; // 0return 0;
}
0
15603116
15628728
0
0
可以在构造函数中使用memset
达到初始化为0的目的。
#include <iostream>
using namespace std;struct Person {int m_age;Person() {memset(this, 0, sizeof(Person)); // this 指向当前对象的地址}
};
// 全局区:成员变量初始化为0
Person g_person;
int main() {// 栈空间Person person; // 堆空间Person *p0 = new Person;// 堆空间Person *p1 = new Person();// 堆空间Person *p = new Person[3] {};cout << g_person.m_age << endl; // 0cout << person.m_age << endl; // 0cout << p0->m_age << endl; // 0cout << p1->m_age << endl; // 0cout << p[0].m_age << endl; // 0
}
0
0
0
0
0
析构函数(Destructor)
析构函数(析构器):在对象销毁的时候自动调用,一般用于完成对象的清理工作
- 函数名以
~
开头,与类同名,无返回值(void都不能写),无参,不可以重载
一个类有且只有一个析构函数
注意:通过malloc
分配的对象free
的时候不会调用析构函数
构造函数、析构函数要声明为public,才能被外界正常使用
#include <iostream>
using namespace std;class Person {int m_age;
public:Person() { // 新的Person对象诞生的象征cout << "Person::Person()" << endl;}~Person() { // 一个Person对象销毁的象征cout << "~Person()" << endl;}
};void test() {Person person; // 局部变量
}int main() {// 指针变量 p 在栈空间// 创建的对象 *p 在堆空间Person *p = new Person; // 调用构造函数// 堆空间的地址cout << p << endl;// 栈空间的地址cout << &p << endl;delete p; // 会调用析构函数getchar();return 0;
}
Person::Person()
0xec15e0
0x61feac
~Person()
内存分析
命名空间(嵌套、合并)
命名空间注意不要产生二义性:
#include <iostream>
using namespace std;namespace MJ{int g_age;
}namespace YU{int g_age;
}int main(){using namespace MH;using namespace YU;g_age = 20; // 报错,产生了二义性,不知道该调用哪个命名空间的g_age
}
命名空间的嵌套:
- 有个默认的全局命名空间
::
,我们创建的命名空间默认都嵌套在它里面
命名空间的合并:以下两种写法等价
通过命名空间的合并,可以将函数的声明与实现分开:
继承性
继承后对象的内存布局:
成员访问权限(public、protected、private)
C++中成员访问权限、继承方式有3种:
- public:公共的,任何地方都可以访问(
struct
默认) - protected:子类内部、当前类内部可以访问
- private:私有的,只有当前类内部可以访问(
class
默认)
子类内部访问父类成员的权限,是以下2项中权限最小的那个
- 成员本身的访问权限
- 上一级父类的继承方式(?)
开发中用的最多的继承方式是 public,这样能保留父类原来的成员访问权限
访问权限不影响对象的内存布局
初始化列表
特点:
- 一种便捷的初始化成员变量的方式
- 只能用在构造函数中
- 初始化顺序只跟成员变量的声明顺序有关
下列两种写法等价:
struct Person {int m_age;int m_height;Person(int age, int height) {this->m_age = age;this->m_height = height;}
};
struct Person {int m_age;int m_height;Person(int age, int height) : m_age(age), m_height(height) {}
};
原因:初始化顺序只跟成员变量的声明顺序有关
- 由于是先声明的
m_age
,再声明的m_height
,因此初始化列表在赋值时,虽然m_height(height)
写在前面,但是首先生效的依旧是m_age(m_height)
,此时 m_height 还没有值,所以 m_age 便被赋了一个未知的值,然后 m_height 被赋值了180。
初始化列表配合默认参数使用:
#include <iostream>
using namespace std;struct Person {int m_age;int m_height;Person(int age = 0, int height = 0) : m_age(age), m_height(height) {}
};int main() {Person person1; // 0 0 cout << person1.m_age << " " << person1.m_height << endl;Person person2(18); // 18, 0cout << person2.m_age << " " << person2.m_height << endl;Person person3(20, 180); // 20, 180cout << person3.m_age << " " << person3.m_height << endl;return 0;
}
如果函数声明和实现是分离的:
- 初始化列表只能写在函数的 实现 中
- 默认参数只能写在函数的 声明 中
构造函数的互相调用
父类的构造函数
子类的构造函数默认会调用父类的无参构造函数
如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
父类指针、子类指针
多态性
默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态
多态是面向对象非常重要的一个特性
- 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
- 在运行时,可以识别出真正的对象类型,调用对应子类中的函数
多态的要素:
- 子类重写父类的成员函数(override)
- 父类指针指向子类对象
- 利用父类指针调用重写的成员函数
虚函数的实现原理:虚表
C++中的多态通过 虚函数(virtual function) 来实现
- 虚函数:被
virtual
修饰的成员函数 - 只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数
(也就是说子类中可以省略virtual
关键字)
虚函数的实现原理是:虚表(虚函数表)
- 虚表里面存储着最终需要调用的虚函数地址
#include<iostream>
using namespace std;class Animal {public:int m_age;virtual void speak() { // 虚函数cout << "Animal::speak()" << endl;}virtual void run() { // 虚函数cout << "Animal::run()" << endl;}
};class Cat : public Animal {public:int m_life;void speak() {cout << "Cat::speak()" << endl;}void run() {cout << "Cat::run()" << endl;}
};int main() {Animal *cat = new Cat(); // 父类指针指向子类对象cat->m_age = 20;cat->speak(); // Cat::speak()cat->run(); // Cat::run()
}
上面代码中的虚表(x86环境)
所有的Cat对象(不管在全局区、栈、堆)共用同一份虚表
虚表汇编分析
虚表(x86环境)
调用父类的成员函数
#include<iostream>
using namespace std;class Animal {public:virtual void speak() {cout << "Animal::speak()" << endl;}
};class Cat : public Animal {public:void speak() {Animal::speak(); // 调用父类的成员函数cout << "Cat::speak()" << endl;}
};int main() {Animal *cat = new Cat(); // 父类指针指向子类对象cat->speak();// Animal::speak()// Cat::speak()
}
虚析构函数
如果存在父类指针指向子类对象的情况,应该将析构函数声明为虚函数(虚析构函数)
delete
父类指针时,才会调用子类的析构函数,保证析构的完整性
纯虚函数
纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范
抽象类(Abstract Class)
- 含有纯虚函数的类,不可以实例化(不可以创建对象)
- 抽象类也可以包含非纯虚函数、成员变量
- 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类
class Animal {virtual void speak() = 0;virtual void walk() = 0;
}
多继承(了解,不建议使用)
C++允许一个类可以有多个父类(不建议使用,会增加程序设计复杂度)
多继承体系下的构造函数调用:
#include<iostream>
using namespace std;class Student {int m_score;
public:Student(int score) : m_score(score) {}
};class Worker {int m_salary;
public:Worker(int salary) : m_salary(salary) {}
};class Undergraduate : public Student, public Worker {public:Undergraduate(int score, int salary) : Student(score), Worker(salary) {};
};int main() {Undergraduate person(100, 9999);return 0;
}
多继承 - 虚函数:
- 如果子类继承的多个父类都有虚函数,那么子类对象就会产生对应的多张虚表
多继承中的同名函数,利用类似命名空间的方法来区分调用:
多继承中的同名成员变量:
菱形继承带来的问题
- 最底下子类从基类继承的成员变量冗余、重复
- 最底下子类无法访问基类的成员,有二义性
虚继承可以解决菱形继承带来的问题:Person类被称为虚基类
静态成员(static)
静态成员:被 static 修饰的成员变量\函数
- 通过对象访问:
car.getCount()
- 通过对象指针访问:
car->getCount()
- 通过类访问:
Car::getCount()
静态成员变量:
- 存储在数据段(全局区,类似于全局变量),整个程序运行过程中只有一份内存
- 对比全局变量,它可以设定访问权限(public、protected、private),达到局部共享的目的
- 必须初始化,必须在类外面初始化,初始化时不能带 static
如果类的声明和实现分离,在实现.cpp中初始化
静态成员函数:
- 内部不能使用 this 指针(this 指针只能用在非静态成员函数内部)
- 不能是虚函数(虚函数只能是非静态成员函数)
- 内部不能访问非静态成员变量\函数,只能访问静态成员变量\函数
- 非静态成员函数内部可以访问静态成员变量\函数
- 构造函数、析构函数不能是静态
- 当声明和实现分离时,实现部分不能带 static
#include<iostream>
using namespace std;class Car {int m_price;static int ms_count;
public:static int getCount() {return ms_count;}Car(int price = 0) : m_price(price) {ms_count++;} ~Car() {ms_count--;}
};int Car::ms_count = 0;int main() {Car *car1 = new Car();Car *car2 = new Car();Car *car3 = new Car();Car *car4 = new Car();cout << Car::getCount() << endl; // 4
}
静态成员应用:单例模式
#include<iostream>
using namespace std;class Rocket {private:Rocket() {} // 构造方法私有化~Rocket() {} // 析构方法私有化static Rocket *ms_rocket;
public:static Rocket *sharedRocket() {// 这里要考虑线程安全if (ms_rocket == NULL) {ms_rocket = new Rocket();}return ms_rocket;}static void deleteRocket() {// 这里要考虑线程安全if (ms_rocket != NULL) {delete ms_rocket;ms_rocket = NULL;}}
};
const 成员
const 成员:被const修饰的成员变量、非静态成员函数
const 成员变量:
- 必须初始化(类内部初始化),可以在声明的时候直接初始化赋值
- 非 static的 const 成员变量还可以在初始化列表中初始化
const成员函数(非静态)
- const 关键字写在参数列表后面,函数的声明和实现都必须带 const
- 内部不能修改非 static 成员变量
- 内部只能调用 const 成员函数、static 成员函数
- 非 const 成员函数可以调用 const 成员函数
- const 成员函数和非 const 成员函数构成重载
- 非 const 对象(指针)优先调用非 const 成员函数
- const 对象(指针)只能调用 const 成员函数、static 成员函数
class Car {const int mc_wheelsCount = 20;
public:Car() : mc_wheelsCount(10) {}void run() const {cout << "run()" << endl;}
}
引用成员类型
引用类型成员变量必须初始化(不考虑 static 情况)
- 在声明的时候直接初始化
- 通过初始化列表初始化
class Car {int age;int &m_price = age;
pubilc:Car(int &pricee) : m_price(price) {}
}
拷贝构造函数
拷贝构造函数是构造函数的一种
当 利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化
拷贝构造函数的格式是固定的:接收一个 const 引用作为参数(必须)
class Car {int m_price;
public: Car(int price = 0) : m_price(price) {}Car(const Car &car) { // 拷贝构造函数, 接收一个 const 引用作为参数this->m_price = car.m_price;}
}
调用父类的拷贝构造函数:
class Person {int m_age;
public:Person(int age) : m_age(age) {}Person(const Person &person) : m_age(person.m_age) {}
};class Student : public Person {int m_score;
public:// 调用父类的拷贝构造函数Student(int age, int score) : Person(age), m_score(score) {}Student(const Student &student) : Person(student), m_score(student.m_score) {}
};
car2、car3都是通过拷贝构造函数初始化的,car、car4是通过非拷贝构造函数初始化:
Car car(100, "BMW 730Li");
Car car2 = car;
Car car3(car2);
car4 = car3; // 赋值操作(默认是浅复制),并不会调用拷贝构造函数
深拷贝、浅拷贝
编译器默认的提供的拷贝是浅拷贝(shallow copy)
- 将一个对象中所有成员变量的值拷贝到另一个对象
- 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
- 可能会导致堆空间多次
free
的问题
如果需要实现深拷贝(deep copy),就需要自定义拷贝构造函数
- 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间
浅拷贝
深拷贝
深拷贝示例
对象型参数和返回值
使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象
匿名对象(临时对象)
匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构
隐式构造
编译器自动生成的构造函数
C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
总结:对象创建后,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数
友元函数、友元类
友元包括:友元函数、友元类
如果将函数A(非成员函数)声明为类C的友元函数,那么函数A就能直接访问类C对象的所有成员
如果将类A声明为类C的友元类,那么类A的所有成员函数都能直接访问类C对象的所有成员
友元破坏了面向对象的封装性,但在某些频繁访问成员变量的地方可以提高性能
内部类
内部类的声明和实现分离:
局部类
【C++快速入门】面向对象篇相关推荐
- java基础快速入门--面向对象(基础)
类与对象 看一个养猫问题 张老太养了两只猫:一只名字叫小白,今年三岁,白色.还有一只叫小花,今年一百岁,花色.请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色.如果用户输入的小猫名 ...
- 使用Cloud DB构建APP 快速入门 - Android篇
概述 此示例应用演示了如何快速的使用Cloud DB构建简单的图书管理服务.通过快速入门和示例应用,您将会了解到如下信息: 如何使用Cloud DB进行应用开发. 应用数据如何写入到Cloud DB. ...
- spring boot 入门_玩转springboot2.x之快速入门开山篇
Spring Boot简介 Spring Boot的目的在于创建和启动新的基于Spring框架的项目.Spring Boot会选择最适合的Spring子项目和第三方开源库进行整合.大部分Spring ...
- IntelliJ IDEA快速入门 | 第一篇:你不会还不知道IntelliJ IDEA吧!不要太low哦!
大家好,我是你们的李阿昀,今天开始,我来给大家讲解一下IntelliJ IDEA的安装.配置与使用. 那IntelliJ IDEA是什么呢?应该说看到这篇文章的小伙伴,应该都知道了,否则的话,你也不会 ...
- 【极客学院】-python学习笔记-Python快速入门(面向对象-引入外部文件-Web2Py创建网站)
极客学院的课程,感觉很有意思,每节课都很短,但是很干货,我喜欢这个节奏 http://www.jikexueyuan.com/course/203.html 课程背景: Python语言功能强大, 能 ...
- 快速入门丨篇五:如何进行运动控制器输入/输出IO的应用?
此前,正运动技术给大家讲了,运动控制器的"固件升级".ZBasic程序开发.ZPLC程序开发以及运动控制器与触摸屏的通讯等,今天我们来学习一下如何进行运动控制器输入/输出I ...
- 快速入门丨篇三:如何进行运动控制器ZPLC程序开发?
此前,正运动技术给大家讲了,运动控制器的"固件升级"以及"ZBasic程序开发",今天我们来学习一下运动控制器ZPLC程序开发. ZPLC是Zmotion运动控 ...
- GoodSync新用户快速入门学习篇
GoodSync是一款非常好用的文件备份与文件同步软件,这款软件可以帮助我们将A中的视频.图片.音频.PDF文档及其他各类型文件以备份同步的方式传输到B中.对于刚接触GoodSync软件的用户而言,可 ...
- JQuery 快速入门一篇通
JQuery是什么? JQuery 是一套JavaScript库, 使用它,可以很方便的进行 JavaScript的编程.比如: 获取页面元素, 修改页面元素的CSS样式等等都可以以很简单的语法完成. ...
- 快速入门丨篇一 如何进行运动控制器固件升级
zfm文件为控制器固件升级包,根据对应的控制器型号选择对应的固件(不同型号的固件包不一样,确保选择正确的固件包,如需固件升级,请联系厂家).可以使用ZDevelop软件或者zfirmdown工具软件下 ...
最新文章
- tensorflow object detection API训练错误解决
- stl 之 copy copy_backward
- centos安装Oracle virtual box
- matlab 1到无穷_从零开始的matlab学习笔记——(6)符号计算与极限
- C++Function Object Adapter之not1
- 评测百万分之一时的精度指标
- allennlp 版本关系
- 大地GhostXP_SP3_2013极速装机5月版
- 量子计算是人工智能的未来吗?
- arduino系列教程之触摸开关(外部中断)开关小灯led
- 配置静态NAT和配置动态NAT
- 科学家用iPS细胞研究阿尔兹海默氏病最新进展
- selenium 淘宝登陆购买,基础实现
- 如何应对外包公司(文思海辉)的Python后端面试
- 京东商品比价分析-数据分析项目
- 0021 arduino iic i2c 实例讲解 TWI是什么 arduino 的 iic 库 i2c库 arduino Wire 库
- 【服务器管理】Ubuntu18.04下安装TensorRT(已经安装CUDA和cudnn的情况下)
- 关于如何把图片放入VS 中并且引用的方法.
- PS5运行Linux,索尼发布新的驱动系统 PS5手柄可支持Linux系统玩家是使用
- oracle 光纤盘,IBM 5773 4GB PCI-E HBA光纤通道卡 10N7249 HBA