面向对象

  • 类与对象
    • 对象的内存布局
    • *this 与 指针访问对象成员的本质
    • 封装性
  • 内存空间的布局
    • 堆空间
    • memset与堆内存的初始化
    • 对象的内存(数据段、栈空间、堆空间)
  • 构造函数(Constructor)
  • 析构函数(Destructor)
  • 命名空间(嵌套、合并)
  • 继承性
  • 成员访问权限(public、protected、private)
  • 初始化列表
  • 父类的构造函数
    • 父类指针、子类指针
  • 多态性
    • 虚函数的实现原理:虚表
    • 调用父类的成员函数
    • 虚析构函数
    • 纯虚函数
    • 多继承(了解,不建议使用)
  • 静态成员(static)
  • const 成员
  • 引用成员类型
  • 拷贝构造函数
    • 深拷贝、浅拷贝
  • 对象型参数和返回值
  • 匿名对象(临时对象)
  • 隐式构造
  • 友元函数、友元类
  • 内部类
  • 局部类

【C++快速入门】基础语法篇

C++命名规范
每个人都可以有自己的编程规范,没有统一的标准,没有标准答案,没有最好的编程规范
变量名规范参考

  • 全局变量:g_xxx
  • 成员变量:m_xxx
  • 静态变量:s_xxx
  • 常量:c_xxx
  • 使用驼峰标识:userName

声明和实现分离

  • 函数的声明可以写在 .h 文件中
  • 函数的实现必须写在 .cpp 文件中

类与对象

C++ 中可以使用 structclass 来定义一个类,两者的的区别:

  • 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 较为方便,实际上 classstruct 除了这一点真的没有任何区别了。

定义了类以后,如何创建对象呢?

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指针的内存都是在函数的栈空间,自动分配和回收

  • 可以尝试反汇编 structclass,看看是否有其他区别(并没有)
  • 实际开发中,一般都用 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_idm_agem_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_idm_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,垃圾值

封装性

成员变量私有化,提供公共的gettersetter给外界去访问成员变量

#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 种地方:

  • 全局区(数据段):全局变量
  • 栈空间:函数里面的局部变量
  • 堆空间:动态申请内存(mallocnew 等)
// 全局区
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 成员变量:

  • 必须初始化(类内部初始化),可以在声明的时候直接初始化赋值
  • staticconst 成员变量还可以在初始化列表中初始化

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++快速入门】面向对象篇相关推荐

  1. java基础快速入门--面向对象(基础)

    类与对象 看一个养猫问题 张老太养了两只猫:一只名字叫小白,今年三岁,白色.还有一只叫小花,今年一百岁,花色.请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色.如果用户输入的小猫名 ...

  2. 使用Cloud DB构建APP 快速入门 - Android篇

    概述 此示例应用演示了如何快速的使用Cloud DB构建简单的图书管理服务.通过快速入门和示例应用,您将会了解到如下信息: 如何使用Cloud DB进行应用开发. 应用数据如何写入到Cloud DB. ...

  3. spring boot 入门_玩转springboot2.x之快速入门开山篇

    Spring Boot简介 Spring Boot的目的在于创建和启动新的基于Spring框架的项目.Spring Boot会选择最适合的Spring子项目和第三方开源库进行整合.大部分Spring ...

  4. IntelliJ IDEA快速入门 | 第一篇:你不会还不知道IntelliJ IDEA吧!不要太low哦!

    大家好,我是你们的李阿昀,今天开始,我来给大家讲解一下IntelliJ IDEA的安装.配置与使用. 那IntelliJ IDEA是什么呢?应该说看到这篇文章的小伙伴,应该都知道了,否则的话,你也不会 ...

  5. 【极客学院】-python学习笔记-Python快速入门(面向对象-引入外部文件-Web2Py创建网站)

    极客学院的课程,感觉很有意思,每节课都很短,但是很干货,我喜欢这个节奏 http://www.jikexueyuan.com/course/203.html 课程背景: Python语言功能强大, 能 ...

  6. 快速入门丨篇五:如何进行运动控制器输入/输出IO的应用?

        此前,正运动技术给大家讲了,运动控制器的"固件升级".ZBasic程序开发.ZPLC程序开发以及运动控制器与触摸屏的通讯等,今天我们来学习一下如何进行运动控制器输入/输出I ...

  7. 快速入门丨篇三:如何进行运动控制器ZPLC程序开发?

    此前,正运动技术给大家讲了,运动控制器的"固件升级"以及"ZBasic程序开发",今天我们来学习一下运动控制器ZPLC程序开发. ZPLC是Zmotion运动控 ...

  8. GoodSync新用户快速入门学习篇

    GoodSync是一款非常好用的文件备份与文件同步软件,这款软件可以帮助我们将A中的视频.图片.音频.PDF文档及其他各类型文件以备份同步的方式传输到B中.对于刚接触GoodSync软件的用户而言,可 ...

  9. JQuery 快速入门一篇通

    JQuery是什么? JQuery 是一套JavaScript库, 使用它,可以很方便的进行 JavaScript的编程.比如: 获取页面元素, 修改页面元素的CSS样式等等都可以以很简单的语法完成. ...

  10. 快速入门丨篇一 如何进行运动控制器固件升级

    zfm文件为控制器固件升级包,根据对应的控制器型号选择对应的固件(不同型号的固件包不一样,确保选择正确的固件包,如需固件升级,请联系厂家).可以使用ZDevelop软件或者zfirmdown工具软件下 ...

最新文章

  1. tensorflow object detection API训练错误解决
  2. stl 之 copy copy_backward
  3. centos安装Oracle virtual box
  4. matlab 1到无穷_从零开始的matlab学习笔记——(6)符号计算与极限
  5. C++Function Object Adapter之not1
  6. 评测百万分之一时的精度指标
  7. allennlp 版本关系
  8. 大地GhostXP_SP3_2013极速装机5月版
  9. 量子计算是人工智能的未来吗?
  10. arduino系列教程之触摸开关(外部中断)开关小灯led
  11. 配置静态NAT和配置动态NAT
  12. 科学家用iPS细胞研究阿尔兹海默氏病最新进展
  13. selenium 淘宝登陆购买,基础实现
  14. 如何应对外包公司(文思海辉)的Python后端面试
  15. 京东商品比价分析-数据分析项目
  16. 0021 arduino iic i2c 实例讲解 TWI是什么 arduino 的 iic 库 i2c库 arduino Wire 库
  17. 【服务器管理】Ubuntu18.04下安装TensorRT(已经安装CUDA和cudnn的情况下)
  18. 关于如何把图片放入VS 中并且引用的方法.
  19. PS5运行Linux,索尼发布新的驱动系统 PS5手柄可支持Linux系统玩家是使用
  20. oracle 光纤盘,IBM 5773 4GB PCI-E HBA光纤通道卡 10N7249 HBA

热门文章

  1. 底薪80万挖来一个大公司高管
  2. 为什么越有钱的人越轻松
  3. 如何用手机NFC代替小区门禁?
  4. Here we want to mention one thing
  5. 1.1.1 计算机网络的概念、组成、功能和分类(转载)
  6. linux分析目录内存,在 Linux x86-64 模式下分析内存映射流程
  7. 联通突然从4g变成3g了_老人机真不能用了?!联通逐渐关闭2G、3G信号服务
  8. 二进制的原码,反码,补码
  9. 翻译Java虚拟机的结构
  10. 2017-2018-1 20155227 《信息安全系统设计基础》第十一周学习总结