1. 什么是面向对象?前言:更多内容请看总纲《嵌入式C/C++学习路》
- 万物皆对象,这是人类面对世界最朴素,最自然的感觉、想法和观点
- 把大型软件看成是一个由对象组成的社会
- 对象拥有足够的智能,能够理解来自其他对象的信息,并以适当的行为作出反应
- 对象能够从高层对象继承属性和行为,并允许低层对象从自己继承属性和行为等
- 编写程序的过程就是描述对象属性和行为的过程,凭借这种能力使问题域和解域获得最大程度的统一
- 面向对象的三大要件:封装、继承和多态
- 至少掌握一种面向对象的程序设计语言,如C++
- 深入理解封装、继承和多态等面向对象的重要概念
- 精通一种元语言,如UML,在概念层次上描述设计。UML类图如下所示
- 学习设计模式,源自多年成功经验的积累和总结
- 拥有相同属性和行为的对象被分成一组,即一个类
- 类可用于表达那些不能直接与内置类型建立自然映射关系的逻辑抽象
- 类是一种用户自定义的复合数据类型,即包括表达属性的成员变量,也包括表达行为的成员函数
- 类是显示世界的抽象,对象是类在虚拟世界的实例
- 在C++中,类和结构已经没有本质型的差别,唯一的不同在于:类class的缺省访问控制属性为私有,结构struct的缺省访问控制属性为公有。
- 访问控制限定符仅作用于类,而非作用于对象,因此同一个类的不同对象,可以互相访问非公有部分
- 对不同成员的访问控制属性加以区分,体现了C++作为面向对象程序设计语言的封装特性
举个简单的例子,假如基类是我,子类是我的老婆儿子,外部是我的同事:
- 公有成员是我的名字,不管是我的家人还是同事朋友都知道
- 保护成员是我的小名,只有我和老婆儿子知道,同事不知道
- 私有成员是我的私房钱,只有我自己知道,老婆儿子同事都不知道
下面通过一段代码学习一下:
#includeusing namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 void who(void) // 行为 成员函数 { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改 { cout << "我在学习" << course << "课程。" << endl; } string m_name; // 成员变量 int m_age; }; // 把学生类变成对象(实例化) int main(void) { Student s1 = {"张三", 22}; // 初始化对象 s1.who(); // 调用成员函数 s1.learn("C++"); s1.m_name = "二货"; // 用户可以随意修改公有成员变量 s1.m_age = -100; s1.who(); // 调用成员函数 }
从输出可以看到,用户可以随意修改公有成员变量,所以出现了私有成员,将名字和年龄放入私有成员以后,再次编译代码将会报错:
#includeusing namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 void who(void) // 行为 成员函数 { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改 { cout << "我在学习" << course << "课程。" << endl; } private: // 私有成员控制访问符 string m_name; // 成员变量 int m_age; }; // 把学生类变成对象(实例化) int main(void) { Student s1 = {"张三", 22}; // 初始化对象 s1.who(); // 调用成员函数 s1.learn("C++"); s1.m_name = "二货"; // 用户可以随意修改公有成员变量 s1.m_age = -100; s1.who(); // 调用成员函数 }
将名字放入私有成员后,用户该怎么给学生对象命名呢?可以在公有成员里面创建一个函数,当学生姓名/年龄满足一定条件的时候,才创建学生对象。因为类的内部可以访问私有成员变量。
例子如下:假如用户给学生命名为”二货“或者年龄 <=0 的时候,拒绝创建。
#includeusing namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 void who(void) // 行为 成员函数 { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改 { cout << "我在学习" << course << "课程。" << endl; } void setName(const string& name){ if(name == "二货"){ cout << "你才是二货!!!" << endl; return ; } m_name = name; } void setAge(int age){ if(age <= 0){ cout << "非法年龄!" << endl; return ; } m_age = age; } private: string m_name; // 成员变量 int m_age; }; // 把学生类变成对象(实例化) int main(void) { Student s1; // 初始化对象 s1.setName("二货"); s1.setAge(-50); s1.who(); // 调用成员函数 s1.setName("张飞"); s1.setAge(22); s1.who(); // 调用成员函数 }
可以看到输入的名字和年龄不满足条件时,s1就是我们初次定义的对象即Student s1;,这里名字是字符串‘ ’,age是随机的数字(这里其实跟代码有关,我们没有对s1进行初始化赋值,因为名字和年龄变成私有变量后,不能通过等号的方式赋值,初始化的方式后面会讲)。下面重新定义合理的名字和年龄后,创建对象成功。
构造函数- 函数名与类名相同,且没有返回类型
- 在创建对象时自动被调用,且仅被调用一次
- ——对象定义语句
- ——new操作符
- 为成员变量赋初值,分配资源,设置对象的初始状态
- 对象的创建过程
- ——为整个对象分配内存空间
- ——以构造实参调用构造函数->构造基类部分->构造成员变量->执行构造代码
下面通过构造函数来初始化对象:
#include类的声明和实现可以分开using namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 Student(const string& name,int age){ // 构造函数 m_name = name; m_age = age; } void who(void) // 行为 成员函数 { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } private: string m_name; // 成员变量 int m_age; }; // 把学生类变成对象(实例化) int main(void) { Student s1("张三",22); s1.who(); Student s2("二货",23); s2.who(); }
class 类名{
返回类型 函数名 (形参表); // 在类的里面只给出函数的声明
};
返回类型 类名::函数名 (形参表){ // 在类的外面给出函数定义
函数体;
}
例如,我把构造函数和who函数的定义放到类外:
#include对象的创建与销毁using namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 Student(const string& name,int age); // 声明 void who(void); private: string m_name; // 成员变量 int m_age; }; // 在类的外面定义函数 Student::Student(const string& name,int age){ m_name = name; m_age = age; } void Student::who(void){ cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } // 把学生类变成对象(实例化) int main(void) { Student s1("张三", 22); s1.who(); Student s2("二货", 23); s2.who(); return 0; }
- 在栈中创建单个对象
类名 对象; // 注意不要加空括号
类名 对象(实参表);
- 在栈中创建对象数组
缺省构造函数类名 对象数组[元素个数];
类名 对象数组[元素个数] = {类名 (实参表),...};
类名 对象数组[ ] = {类名 (实参表),...};
- 有时必须自己定义缺省构造函数,即使它什么也不做,尤其是在使用数组或容器的时候,某些基于早期C++标准的编译器不支持对象数组的初始化语法
- 有时必须为一个类提供缺省构造函数,仅仅因为它可能作为另一个类的子对象而被缺省构造
- 若子对象不宜缺省构造,则需要为父对象提供缺省构造函数,并在其中显式地以非缺省方式构造该子对象
下面为Student构造函数加入缺省参数:
#includeusing namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 Student(const string& name = "新人",int age = 18); // 声明 加入缺省参数 void who(void); private: string m_name; // 成员变量 int m_age; }; // 在类的外面定义函数 Student::Student(const string& name,int age){ m_name = name; m_age = age; } void Student::who(void){ cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } // 把学生类变成对象(实例化) int main(void) { Student s1("张三", 22); s1.who(); Student s2("二货", 23); s2.who(); Student s3; // 创建一个学生对象不进行初始化,默认取缺省参数的值 s3.who(); return 0; }
下面创建一个对象数组:
#includeusing namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 Student(const string &name = "新人", int age = 18); // 声明 void who(void); void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改 { cout << "我在学习" << course << "课程。" << endl; } void setName(const string &name) { if (name == "二货") { cout << "你才是二货!!!" << endl; return; } m_name = name; } void setAge(int age) { if (age <= 0) { cout << "非法年龄!" << endl; return; } m_age = age; } private: string m_name; // 成员变量 int m_age; }; // 在类的外面定义函数 Student::Student(const string &name, int age) { m_name = name; m_age = age; } void Student::who(void) { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } // 把学生类变成对象(实例化) int main(void) { // 采用无参方式构造数组 Student sa[3]; for (size_t i = 0; i < 3; i++) { sa[i].who(); } // 采用有参方式构造数组 Student sc[3] = { // 这里括号里的3也可以不写 Student("张飞", 22), Student("马超", 23), Student("鲁班", 2)}; for (size_t i = 0; i < 3; i++) { sc[i].who(); } return 0; }
- 在堆中创建/销毁单个对象
类名*对象指针 = new 类名;
类名*对象指针 = new 类名();
类名*对象指针 = new 类名(实参表);
delete 对象指针
- 在堆中创建/销毁对象数组
类名* 对象指针数组 = new 类名[元素个数];
类名* 对象指针数组 = new 类名[元素个数]{ 类名 (实参表),....};
// 上面的写法需要编译器支持C++11标准,关于C++标准,请参考《C++语言标准》
delete[ ] 对象数组指针;
具体示例如下:
#include将类的声明、实现与使用分别放在不同文件里using namespace std; class Student // 创建学生类 { public: // 公有成员访问控制符 Student(const string &name = "新人", int age = 18); // 声明 void who(void); private: string m_name; // 成员变量 int m_age; }; // 在类的外面定义函数 Student::Student(const string &name, int age) { m_name = name; m_age = age; } void Student::who(void) { cout << "我是" << m_name << ",今年" << m_age << "岁." << endl; } // 把学生类变成对象(实例化) int main(void) { // 在堆中创建单个对象 Student* ps = new Student; ps->who(); delete ps; ps = new Student(); // 这个括号可加可不加,看个人习惯 ps->who(); delete ps; ps = new Student("张飞",22); ps->who(); delete ps; cout << "-------------------------------" << endl; // 在堆中创建对象数组,无参数 Student* pw = new Student[3]; for (size_t i = 0; i < 3; i++) { pw[i].who(); } // 销毁数组 delete[] pw; cout << "-------------------------------" << endl; // 有参方式创建数组 Student* pe = new Student[3]{ Student("鲁班",2),Student("孙尚香",19),Student("刘备",20) }; for (size_t i = 0; i < 3; i++) { pe[i].who(); } delete[] pe; return 0; }
在实际工作中都是分开的,如下图:
将上述Student分开写为:先看一下整体的目录:
头文件student.h的内容,存放的是Student类的声明:
// Student类的声明 #ifndef _STUDENT_H #define _STUDENT_H #includeusing namespace std; class Student{ public: Student(const string& name = "新成员",int age = 20); void who(void); void learn(const string& course); void setName(const string& name); void setAge(int age); private: string m_name; int m_age; }; #endif // _STUDENT_H
再看student.cpp的内容,存放的是Student类的实现:
// Student类的实现
#include "student.h"
Student::Student(const string& name, int age)
{
m_name = name;
m_age = age;
}
void Student::who(void)
{
cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}
void Student::learn(const string& course)
{
cout << "我在学习" << course << "课程。" << endl;
}
void Student::setName(const string &name)
{
if (name == "二货")
{
cout << "你才是二货!!!" << endl;
return;
}
m_name = name;
}
void Student::setAge(int age)
{
if (age <= 0)
{
cout << "非法年龄!" << endl;
return;
}
m_age = age;
}
然后是main.cpp,使用Student类:
//使用Student类
#include "student.h"
int main(void){
Student luban("鲁班",2);
luban.who();
luban.learn("二级能接闪现");
return 0;
}
编译并输出如下:
值得注意的是,在编译的时候我在后面加上了student.cpp,正常来说直接编译main.cpp就好了,但是我遇到了一个错误:
如果你也遇到了和我一样的问题,可以参考这篇文章:《"undefined reference to XXX"问题总结》https://zhuanlan.zhihu.com/p/81681440
练习:编写一个动态时钟,在终端显示当前时间,有兴趣可以看一下-->链接
构造函数可以重载- 构造函数也可以通过参数表和差别化形成重载
- 重载的构造函数通过构造实参的类型选择匹配
- 不同的构造函数版本表示不同的对象创建方式
- 使用缺省参数可以减少构造函数重载版本数量
- 某些构造函数具有特殊的意义:
- ——缺省构造函数:按缺省方式构造
- ——类型转换构造函数:从不同类型的对象构造
- ——拷贝构造函数:从相同类型的对象构造
来看一个重载的例子:
#includeusing namespace std; class Student { public: // 下面的四个构造函数属于重载关系 Student(void) { m_name = "新人"; m_age = 20; } Student(const string &name) { m_name = name; m_age = 20; } Student(int age) { m_name = "新人"; m_age = age; } Student(const string &name, int age) // 这个函数可以代替这四个函数 { m_name = name; m_age = age; } void show(void) { cout << m_name << "," << m_age << endl; } private: string m_name; int m_age; }; int main() { Student s1; s1.show(); Student s2("张飞"); s2.show(); Student s3(30); s3.show(); Student s4("赵云", 12); s4.show(); }
那么为什么说使用缺省参数可以减少构造函数重载版本的数量呢?其实上面的例子中,四个重载版本只需要最后一个版本即可,因为里面有两个缺省参数,覆盖了前面的三种情况。
缺省构造函数- 缺省构造函数也称无参构造函数,但其未必真的没有任何参数,为一个有参构造函数的每个参数都提供一个缺省值,同样可以达到无参构造函数的效果
- 如果一个类没有定义任何构造函数,那么编译器会为其提供一个缺省构造函数
- 对基本类型的成员变量(int,char这种),不做初始化
- 对类类型(比如我们定义的Student类)的成员变量和基类子对象,调用相应类型的缺省构造函数初始化
- 对于已经定义至少一个构造函数的类,无论其构造函数是否带有参数,编译器都不会为其提供缺省构造函数
- 有时必须自己定义缺省构造函数,即使它什么也不做,尤其是在使用数组或容器的时候,某些基于早期C++标准的编译器不支持对象数组的初始化语法
- 有时必须为一个类提供缺省构造函数,仅仅因为他可能作为一另一个类的子对象而被缺省构造
- 若子对象不宜缺省构造,则需要为父对象提供缺省构造函数,并在其中显式地以非缺省方式构造该子对象
来看一个例子:
#include类型转换构造函数using namespace std; class A { public: A(int a) {} }; class B // 没有构造函数,系统自己定义 { public: B(void) : m_a(10) {} // 初始化表,显式,非缺省构造方式构造m_a private: A m_a; // B里面有A类型的成员变量 }; int main() { B b; // A类的A是B类的子对象 return 0; }
- 在目标类型中,可以接受单个源类型对象实参的构造函数,支持从源类型到目标类型的隐式类型转换
class 目标类型{
目标类型(const 源类型& src){....}
};
- 通过explicit关键字,可以强制这种通过构造函数实现的类型转换必须显式地进行
class 目标类型{
explicit 目标类型(const 源类型& src){....}
};
来看一个例子:将小狗变成小猫:
#includeusing namespace std; class Dog; // 声明 class Cat { public: Cat(const string &name) { m_name = name; } // 类型转换操作函数 Cat是转换目标 Dog是转换源 explicit Cat(const Dog& dog); // 声明 explicit是显式的,不加表示隐式 void talk(void) { cout << m_name << ": 喵~~~喵~~~" << endl; } private: string m_name; }; class Dog { public: Dog(const string &name) { m_name = name; } void talk(void) { cout << m_name << ": 汪汪!!" << endl; } private: string m_name; friend class Cat; // 友元,Cat可以访问Dog的私有成员 }; Cat::Cat(const Dog& dog){ // 类外定义 dog可以转换为cat m_name = dog.m_name; }; int main(void) { Dog dog("小白"); dog.talk(); // 用static_cast显式地完成 // Cat cat = dog; 隐式 Cat cat = static_cast (dog); // 不同源 !!!!!!!!!!!!!11 cat.talk(); Cat cat2("咪咪"); cat2.talk(); //cat2 = dog; 隐式 cat2 = static_cast (dog); // !!!!!!!!!!!!1 cat2.talk(); return 0; }
输出如下:实际中常用的为显式类型转换,即需要加explicit关键字,隐式类型转换的语句已在代码中注明
拷贝构造函数- 用于从一个已定义的对象构造其同类型的副本,即对象克隆,格式如下:
class 类名{
类名(const 类名& that){ ... }
};
- 如果一个类没有定义拷贝构造函数,那么编译器会为其提供一个缺省拷贝构造函数
- 对基本类型成员变量,按字节复制
- 对类类型成员变量和基类子对象,调用相应类型的拷贝构造函数
- 所有系统定义的构造函数,其访问属性都是公有public
- 如果自己定义了拷贝函数,编译器不再提供缺省拷贝构造函数,这时所有与成员复制有关的操作,都必须在自定义拷贝构造函数中编写代码完成
- 若缺省拷贝构造函数不能满足要求,则需自己定义
- 拷贝构造的时机(重要!一定要知道):
- 用已定义对象作为同类型对象的构造实参
- 以对象的形式向函数传递参数
- 从函数中返回对象
- 某些拷贝构造过程会因编译优化而被省略
| 自定义构造函数 | 系统定义构造函数 |
| 无 | 缺省构造函数 缺省拷贝构造函数 |
| 除拷贝构造函数意外的任何构造函数 | 缺省拷贝构造函数 |
| 拷贝构造函数 | 无 |
下面看一个拷贝构造的例子:
#includeusing namespace std; class Student { public: Student(const string &name, int age) { m_name = name; m_age = age; } // 拷贝构造函数 Student(const Student& that){ m_name = that.m_name; m_age = that.m_age; } void show(void){ cout << m_name << " " << m_age << endl; } private: string m_name; int m_age; }; void foo(Student s){ s.show(); } int main(void) { Student s1("张飞", 22); s1.show(); Student s2(s1); // s2就是s1的克隆 s2.show(); // 以对象的形式向函数传参,引发拷贝构造 foo(s2); // 这也是拷贝手法,即foo的参数s是s2的拷贝 return 0; }



