目录
一、典型的C++面向对象编程
1、基本组成
(1)头文件hpp中类的定义
(2)源文件cpp中类的实现(构造函数、析构函数、方法)
(3)主程序
2、案例
(1)用C++来编程“人一天的生活”
(2)“人”的属性:name、age、male
(3)“人”的方法:eat、work(coding/shopping)、sleep
(4)人的生活:eat1->work->eat2->work->sleep
实战中,一般一个cpp和一个hpp文件配对,描述一个class,class的名字和文件名相同的。
3、C++面向对象式编程总结
(1)整个工作分为2大块:一个是建模和编写类库,一个是使用类库来编写主程序完成任务。
(2)有些人只负责建模和编写类库 。
(3)有些人直接调用现成类库来编写自己的主任务程序,譬如使用opencv分析一张图片中有没有电动车
(4)难度上不确定,2个都可能很难或者很简单。
//person.hpp文件
#ifndef __PERSON_H__
#define __PERSON_H__
#include <string>
using namespace std;
namespace MAN
{
class person
{
public:
//属性
string name;
int age;
int male;//1表示男,0表示女
//方法
void work(void);
void eat(void);
void sleep(void);
private:
};
}
//person.cpp文件
#include "person.hpp"
#include <iostream>
using namespace std;
void MAN::person::work(void)
{
if (this->male)
{
cout << "working is coding" << endl;
}
else
{
cout << "shopping" << endl;
}
}
void MAN::person::eat(void)
{
cout << "eating food and fruits." << endl;
}
void MAN::person::sleep(void)
{
cout << "sleeping.age = " << age << endl;
}
//main.cpp
#include "person.hpp"
using namespace MAN;
int main(int argc, char *argv[])
{
person zhangsan;
zhangsan.name = "zhangzheng";
zhangsan.age = 22;
zhangsan.male = 1;
zhangsan.eat();
zhangsan.work();
zhangsan.sleep();
return 0;
}
4、C++学习的三重境界
(1)学习C++第一重境界就是语法层面,先学会如何利用C++来建模、来编程,学习语法时先别解决难度大问题。
(2)学习C++第二重境界是解决问题层面,学习如果理解并调用现成类库来编写主程序解决问题。
(3)学习C++第三重境界是编写类库和sample给别人用,需要基础好且有一定架构思维。
二、C++的构造函数和析构函数
1、什么是构造函数
(1)constructor,字面意思是用来构造对象的函数;destructor,字面意思是用来析构对象的函数
(2)可以理解为语言自带的一种hook函数(回调函数)
回调函数就是一个被作为参数传递的函数。回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
(3)当对象产生时constructor会自动被调用,一般用于初始化class的属性、分配class内部需要的动态内存
(4)对对象消亡时destructor会自动被调用,一般用于回收constructor中分配的动态内存,避免内存丢失
2、构造和析构一般用法
(1)不写时C++会自动提供默认的构造和析构,也可以显式提供默认构造和析构函数
(2)构造和析构函数不需要返回值类型,构造函数可以带参或不带参,析构函数不带参
(3)构造函数可以重载(overload),析构函数不需要重载
3、为什么需要构造函数和析构函数
(1)构造函数可以看作是对象的初始化式,注意对比对象和变量的初始化区别
(2)构造函数可以为对象完成动态内存申请,同时在析构函数中再释放,形成动态内存的完整使用循环。
(3)C语言中struct无构造函数概念,所以struct中需要用到动态内存时必须在定义struct变量后再次单独申请和释放,而这些操作都需要程序员手工完成
(4)C++ class的构造和析构特性,是C++支持面向对象编程的一大语言特性。
三、在构造和析构函数中使用动态内存
1、析构函数的使用
(1)析构函数在对象被销毁时自动调用,一般有2种情况
(2)用new分配的对象,用delete显式析构,才会调用析构函数
(3)分配在栈上的对象(例如局部变量),当栈释放时自动析构
(4)普通情况下析构函数都是空的,因为不必做什么特别的事情
2、在class中使用动态内存变量
(1)什么情况下用动态内存?
需要大块内存,且需要按需灵活的申请和释放,用栈怕爆、用全局怕浪费和死板时
(2)在class person中增加一个int *指针,用于指向一个int类型元素的内存空间
(3)在构造函数中分配动态内存
(4)在析构函数中回收动态内存
(5)将动态内存从int变量升级到int数组变量
(6)实战中C++常用的动态内存往往是容器vector那些,之后的文章会讲到
3、用valgrind工具查看内存泄漏
(1)valgrind工具介绍:参考:https://blog.csdn.net/u012662731/article/details/78652651
(2)安装:sudo apt-get install valgrind(ubuntu16.04 X64)
(3)编译程序:主要是添加-g参数便于调试时有行号 g++ person.cpp main.cpp -g -o apptest
(4)使用:valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes ./可执行程序名
//person.hpp文件
#ifndef __PERSON_H_
#define __PERSON_H_
//要学会使用条件宏从而避免重复包含,否则会出现一些奇怪的错误,如
//person()的重载不支持,会报错,只能有一个person函数
#include <string>
using namespace std;
namespace MAN{
class person;//声明这个类
class person
{
public:
//属性
string name;
int age;
int male;//0表示女,1表示男
//构造与析构函数
person();//默认构造函数
person(string name);//自定义构造函数
~person();//默认析构函数
//方法
void eat(void);
void sleep(void);
void work(void);
private:
};
}
#endif
//person.cpp
#include "person.hpp"
#include <iostream>
using namespace std;
void MAN::person::eat(void)
{
cout << name << "eat" << endl;
}
void MAN::person::sleep(void)
{
cout << name << "sleeping" << endl;
}
void MAN::person::work(void)
{
if (this->male)
{
cout << name << "coding" << endl;
}
else
{
cout << name << "shopping" << endl;
}
}
MAN::person::person()
{
//默认构造函数是空的
cout << "构造函数" << endl;
}
MAN::person::person(string name)
{
this->name = name;//构造对象后同时对对象的属性初始化
cout << "构造函数" << endl;
}
MAN::person::~person()
{
//默认析构函数也是空的
cout << "析构函数" << endl;
}
#include "person.hpp"
using namespace MAN;
int main(int argc, char *argv[])
{
string s1 = "linux";
//person *pPerson = new person;
person *pPerson = new person(s1);
//pPerson->name = "zhangsan";
pPerson->age = 23;
pPerson->male = 1;
pPerson->eat();
pPerson->work();
pPerson->sleep();
return 0;
}
四、构造函数与类的成员初始化
1、构造函数一大功能就是初始化成员变量
(1)默认构造函数不带参,无初始化功能
(2)若无其他构造函数,则默认构造函数可以省略。但若有个其他构造函数但还想使用默认构造函数,则默认构造函数不能省,必须写上。
(3)栈上分配对象时,若使用默认构造函数,则对象变量后面不加空的(),若用带参构造才需要加"(初始化参数)"
2、C++的成员初始化列表
Person::Person(string my_name, int my_age, bool my_male):name(my_name),age(my_age),male(my_male)
{//使用构造函数的参数给属性赋值
}
class CExample {
public:
int a;
float b;
example 1,构造函数初始化列表
CExample(): a(0),b(8.8)
{}
example 2,构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化
(1)一般用于带参构造函数中,用来给属性传参赋值
(2)成员初始化列表和构造函数之间用冒号间隔,多个列表项之间用逗号间隔
(3)初始化列表可以替代构造函数内的赋值语句,达到同样效果
3、构造函数使用参数默认值
(1)class声明时可以给函数形参赋值一个默认值,实际调用时若不传参就使用默认值
(2)方法实现时形参可以不写默认值,但是实际是按照声明时的默认值规则的
(3)有默认值情况,要注意实际调用不能有重载歧义,否则编译不能通过
要注意这样一种歧义情况:
person(){};
person(string myname = "linux", int myage = 10, bool male = true);
这样去创建对象时:person pPerson;无法判断你是使用的无参数构造函数还是使用有了参数的并且设定了默认值的构造函数。
还有一种情况也会出现歧义:
person(string myname, int myage);
person(string myname, int myage, bool male = true);
因为第二个的最后一个形参有默认参数可以不传参,所以就会造成重载上的歧义。
函数重载不考虑参数的默认值,只看参数多个形参情况下,若有一个形参有默认参数,则其之后的形参也必须有默认参数,其之前的可以不必有。否则会报错。
(4)所有参数都带默认值的构造函数,1个可以顶多个构造函数(举例说明)
通过传的参数个数和默认参数的结合实现多种情况;
person(string myname = "linux", int myage = 10, bool male = true);
person(s1)
person(string myname, int myage = 10, bool male = true);
person(s1, 23)
person(string myname, int myage, bool male = true);
五、拷贝构造函数的引入
1、用对象来初始化对象
(1)简单变量定义时,可以直接初始化,也可以用另一个同类型变量来初始化(间接初始化)。举例说明
int a = 10;//直接初始化
int b = a;//间接初始化
(2)用class来定义对象时,可以直接初始化,也可以用另一个对象来初始化。举例说明
Person p1(“linux”, 100, 0);//直接初始化
Person p2(p1); //间接初始化
Person p2 = p1; //间接初始化
2、为什么可以
(1)变量的直接初始化,是变量在被分配内存之后直接用初始化值去填充赋值完成初始化
(2)变量用另一个变量来初始化,是给变量分配了内存后执行了一个内存复制操作来完成的初始化
(3)对象的直接初始化,是对象在分配内存之后调用了相应constructor来完成的初始化
(4)对象用另一个对象来初始化,是对象在分配之后调用了相应的copy constructor来完成初始化
3、拷贝构造函数
(1)拷贝构造函数是构造函数的一种,符合构造函数的一般性规则
(2)拷贝构造函数的引入是为了让对象在初始化时能够像简单变量一样的被直接用"="来赋值
(3)拷贝构造函数不需要重载,他的参数列表固定为const classname& xx
&表示引用
(4)拷贝构造函数很合适用初始化列表来实现
默认提供的拷贝函数会自动去完成拷贝,而我们自己写的拷贝函数需要自己去进行拷贝。
//person.hpp文件
#ifndef __PERSON_H_
#define __PERSON_H_
#include <string>
using namespace std;
namespace MAN
{
//声明这个类
class person
{
//访问权限
public:
//属性
string name;
int age;
bool male;
//构造与析构函数
person(string myname, int myage, bool mymale);
person(const person& pn);
~person();
int *p;
int *p1;
//方法
void eat(void);
void work(void);
void sleep(void);
void print(void);
private:
};
}
//person.cpp
#include "person.hpp"
#include <iostream>
#include <stdlib.h>
using namespace std;
using namespace MAN;
void person::eat(void)
{
cout << name << "eat" << endl;
}
void person::work(void)
{
if (this->male)
{
cout << this->name << " coding" << endl;
}
else
{
cout << this->name << " shopping" << endl;
}
}
void person::sleep(void)
{
cout << this->name << "sleep" << endl;
}
// 打印出对象中所有成员的值
void person::print(void)
{
cout << "name = " << name << endl;
cout << "age = " << age << endl;
cout << "male = " << male << endl;
cout << "p = " << p << " p size:" << sizeof(p) <<endl;
cout << "*p1 = " << *p1 << endl;
cout << "-----------" << endl;
}
person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)
//MAN::person::person(string myname, int myage, bool mymale)
{
this->p = new int[100];
this->p1 = new int(5);
cout << "userdefined constructor" << endl;
}
//拷贝构造函数
person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)
{
/*
//使用初始化成员列表和以下方法的效果相同
this->name = pn.name;
this->age = pn.age;
this->male = pn.male;
*/
this->p = new int(sizeof(pn.p));
this->p1 = new int(*pn.p1);
}
person::~person()
{
delete[] this->p;
delete this->p1;
cout << "userdefined destructor" << endl;
}
#include "person.hpp"
using namespace MAN;
int main(int argc, char *argv[])
{
//方式一:直接初始化
person p1("astyon", 35, true);
//方式二:用另一个对象来初始化新定义的对象
//person p2=p1;
person p2(p1);
p1.print();
p2.print();
return 0;
}
六、浅拷贝与深拷贝
1、浅拷贝的缺陷
(1)只有普通成员变量初始化的拷贝构造函数,就是浅拷贝
(2)如果不显式提供,C++会自动提供一个全部普通成员被浅拷贝的默认copy constructor
(3)浅拷贝在遇到有动态内存分配时就会出问题
2、如何解决
(1)不要用默认copy constructor,自己显式提供一个copy constructor,并且在其内部再次分配动态内存
(2)这就叫深拷贝,深的意思就是不止给指针变量本身分配内存一份,也给指针指向的空间再分配内存(如果有需要还要复制内存内的值)一份
(3)一般如果不需要深拷贝,根本就不用显式提供copy constructor,所以提供了的基本都是需要深拷贝的
(4)拷贝构造函数不需要额外的析构函数来对应,用的还是原来的析构函数
3、如何深度理解浅拷贝和深拷贝
(1)这个问题不是C++特有的,Java等语言也会遇到,只是语言给封起来了,而C++需要类作者自己精心处理
(2)从编程语言学角度讲,本质上是值语义value symatics和引用语义reference symatics的差别
(3)C学好了有linux内核阅读级别的C功底,理解这些简直太简单了
七、什么是访问权限问题
1、public访问权限是全局的
(1)public的成员变量,在类的成员方法中可以直接访问
(2)public的成员变量,在任何外部代码中可以通过类的对象来直接访问
(3)public的成员方法,在类内其他成员方法中可以直接调用
(4)public的成员方法,在任何外部代码中可以通过类的对象来直接访问
(5)public就是完全不设防
2、private访问权限
(1)private的成员变量,在类的成员方法中可以直接访问
(2)private的成员变量,在任何外部代码中不可以通过对象来直接访问
(3)private的成员方法,在类内其他成员方法中可以直接调用
(4)private的成员方法,在任何外部代码中不可以通过对象来直接访问
(5)private就是对内不设防,对外完全设防的
//private.cpp
#include <iostream>
#include <string>
using namespace std;
namespace MAN
{
class person
{
public:
string name;
unsigned int age;
bool male;
void work(void);
void sleep(void);
void read_private(void);
person(string name, unsigned int age, bool male);
~person();
private:
int grade;
void salary(void);
};
}
using namespace MAN;
void person::work(void)
{
cout << "This is in work function." << endl;
salary();
this->grade = 10;
cout << this->name << "is working." << endl;
}
void person::salary(void)
{
cout << "you should guess it." << endl;
}
void person::sleep(void)
{
cout << this->name << "is sleeping." << endl;
}
void person::read_private(void)
{
cout << this->grade << endl;
}
person::person(string pname, unsigned int page, bool pmale):name(pname),age(page),male(pmale)
{
cout << "constructor." << endl;
}
person::~person()
{
cout << "destructor" << endl;
}
int main(int argc, char *argv[])
{
person human("zhangSan", 22, true);
human.work();
human.read_private();
human.sleep();
return 0;
}
3、更多关于访问权限问题的问题
(1)protected是第三种访问权限修饰符,如果有必要还可以有第4种甚至更多
(2)访问权限叠加类的继承、static、friend等特性后才更加显出复杂性和威力
(3)暂时只先引入这个概念,掌握基本用法和规则即可,后面逐步去深入
八、C++为什么要设计访问权限
1、访问权限作用1:保护内部资源
(1)private的成员是class内部使用,外部没必要直接访问(读取或赋值),所以干脆在语法上让你看不见
(2)访问权限的保护是一种语法层面的保护,而非硬件上限制访问,硬件做不了这么细致
(3)最终目的也是为了整个程序更安全
2、访问权限作用2:隐藏外部无需关心的细节
(1)将class内部使用而外部绝不会用到的成员变量隐藏起来,以免干扰外部或者不小心被外部修改了
(2)隐藏细节可以降低使用类库的人的难度,调用时只看到对我有用的东西
(3)这个隐藏其实是把class的作者和使用者专业分工,是很有必要的
3、这就是面向对象的封装特性
(1)封装特性的体现之一就是抽象,抽象的一层意思就是隐藏掉不必要的细节
(2)封装特性的体现之一就是组织和保护,便于整个整体和外部更合理的交流
九、访问权限带来的一些新编程理念
1、只读或只写变量
(1)问题:你的class中需要一个变量,但是希望外部只能读不能写,怎么实现?
(2)分析:硬件、OS、编程语言都未提供这样的RO或WO的机制,只能间接实现了
(3)解决方案:利用访问权限,将成员变量设计为private,然后再通过封装成员方法来实现
(4)如果只提供read方法那就是只读成员,如果只提供write方法那就是只写成员
eg:上方的private.cpp
(5)这样的成员经常被称为属性(property),C#等高级编程语言中就源生支撑这种新编程思想
2、复杂程序架构化
(1)简单程序规模小,参与人少,靠人本身的逻辑能力就能保证实现和质量
(2)复杂程序规模大参与人多,协作成本高,水平良莠不齐,必须靠架构式设计才能保证实现和质量
(3)架构化设计中权限管控的思想很重要,每一层的访问权限和主要关注点都不同
3、为什么C语言不需要这些
(1)C主要做裸机开发和OS内核开发,都是独立一体化程序,不隔离,所以不需要也没法管控权限
(2)C程序一般规模小,不需要管控权限
(3)C程序强调性能,而权限管控一定程度会牺牲性能(存储、运行速度等)。凡事都有两面性。
(4)越是高级语言,编程越偏业务逻辑,就越需要权限管控和架构思想这些。
十、struct和class的区别
1、C和C++中struct的区别
(1)C中不支持成员函数(只能通过函数指针成员变量间接支持),而C++源生支持。
(2)C中不支持static成员,而C++中支持。后面会详细讲,C++ static class是一个大知识点
(3)访问权限,C中默认public,C++中默认public,但是可以显式指定public/private/protected三者之一
(4)继承特性上,C中不支持(只能通过结构体包含来间接实现),而C++源生支持,且
struct和class可以互相继承
(5)初始化方面,C中靠初始化式(gcc扩展了初始化语法),而C++靠构造函数所以初始化更
自由可定制化
2、C++中struct和class的区别
(1)默认成员权限,struct默认public,class默认private
(2)继承关系的权限管控,struct默认public,class默认private
(3)struct和class交叉继承时,默认的权限管控取决于子类而不是基类
(4)模板相关使用都用class,而不用struct了
3、总结
(1)C++中struct和class差别不大,大多数情况下都可以直接替换使用而不出错
(2)C++中struct其实有点“人格分裂”,他既要兼容C中struct,又要像C++的class当struct有了一个构造函数,他C的初始化方式就不能用了。
(3)结论:除非是只需要打包几个变量数据就用C方式的struct,否则如果需要面向对象式编程就用class
十一·、const和mutable是干嘛的
1、const可以实现常函数
(1)常函数,就是class的成员函数承诺在函数内部不会修改class的任何成员变量,注意是任何一个
(2)思考:C++为什么设计常函数?
为了class的设计者和使用者更好的协作,避免错误的使用类库c++规定,使用函数的参数是一个const类型的引用,则该引用包括的所有成员方法必须是常方法,即常函数
2、mutable可以局部打破const常函数
(1)const常函数承诺在函数内不会修改class的任何一个成员变量
(2)但是有时候个别成员变量,就是需要在const的常函数中也能修改(注意只是个别,其他大部分是不需要修改的)
(3)怎么办?
2个解法:要么去掉const,要么使用mutable局部打洞
(4)const、mutable使用演示
//person.hpp
#ifndef __PERSON_H_
#define __PERSON_H_
#include <string>
using namespace std;
namespace MAN
{
class person;//前置声明
class person//声明这个类
{
int salary;
public:
string name;
int age;
void work(void);
void add_age(void);
void radd_age(void) const;
private:
int grade;
mutable int great_grade;
protected:
};
}
#endif
//person.cpp
#include "person.hpp"
#include <iostream>
using namespace MAN;
using namespace std;
void person::work(void)
{
cout << this->name << "is working." << endl;
}
void person::add_age(void)
{
this->age++;
this->great_grade =149;
cout << "age =" << this->age << endl;
}
void person::radd_age(void) const
{
//this->age++;//这个是不行的,可以取消注释,编译看看实际效果
this->great_grade++;
//cout << "age =" << this->age << endl;
cout << "great_grade =" << this->great_grade << endl;
}
//main.cpp
#include "person.hpp"
using namespace MAN;
void test(const person& pn)
{
pn.radd_age();
}
int main(int argc, char *argv[])
{
person human;
human.name = "zhangsan";
human.age = 22;
//human.grade = 59;
//human.great_grade = 150;
human.work();
human.add_age();
test(human);
}
(5)思考:C++为什么设计mutable?
和private那里一样,还是“先全部禁了再按需打开”的思路。
3、class的前置声明
(1)就是class的声明,安慰编译器的
(2)看到了认识即可
//前置声明
class person;
// 声明这个类
class person
{
....
}
4、inline member function
(1)类的声明中直接写函数体,则此函数会被编译器inline化处理
(2)类的声明中正常处理,而成员函数的实现中加inline
(3)inline的成员函数应该放在hpp中而不是cpp中,这个一定要注意,因为inline是在编译时替换的
因为C/C++编译时是以当前.c文件为单位的,只考虑当前c文件,如果将inline函数放在另一个文件中,而在这个文件中有使用,编译时是会报错的,提示无法找到函数体,只有在c文件都编译完链接的时候才会发现,所以要将其放在头文件中。
5、本篇文章总结
(1)本篇文章主要在讲C++的封装特性,以及由此引发的各种细节语法特性。
(2)语法上掌握是第一层,会写代码跑起来是第二层,遇到问题能调出来是第三层,理解设计原理是第四层。
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来并且引用了部分他人博客的内容,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。