类与对象——Object-Oriented
(1)Abstraction (抽象) (2) Polymorphism (多态) (3) Inheritance (继承) (4) Encapsulation (封装)
protected、private、public(keywords)
- To specify whether data fields and functions can be accessed from the outside of the class. (说明数据及函数是否可以从类外面访问)
- Private members can only be accessed from the inside of the class (私有成员只能在类内的函数访问)
- Public members can be accessed from any other classes. (公有成员可被任何其他类访问)
- A protected data field or a protected function in a base class can be accessed by name in its derived classes. (保护属性的数据或函数可被派生类成员访问)
Constructors(构造函数):
(1) Automatic invocation(自动调用)
(2) Has the same name as the defining class (与类同名)
(3) No return value (including "void")(无返回值)
(4) Can be overloaded (可重载)
(5) May have no arguments (可不带参数)
(6) A class may be declared without ctors (类可不声明构造函数)
A no-arg constructor with an empty body is implicitly declared in the class.(编译器会提供一个带有空函数体的无参构造函数)
This constructor, called a default constructor is provided automatically only if no constructors are explicitly declared in the class.(只有当未明确声明构造函数时,编译器才会提供这个构造函数,并称之为“默认构造函数”)
构造函数的初始化列表
类的数据域是一个对象类型,被称为对象中的对象,或者内嵌对象。内嵌对象必须在被嵌对象的构造函数体执行前就构造完成,所以有了构造函数的初始化列表。
ClassName (parameterList)
: dataField1{value1}, dataField2{value2}
{
// Something to do
}
规定:对象类型数据域必须在构造函数体之前初始化
class Time
{ /* Code omitted */
}
class Action
{
private:
Time time;
public:
Action(int hour, int minute, int second)
{
time = Time(hour, minute, second); //time对象应该在构造函数体之前构造完成
}
};
Action a(11, 59, 30);
默认构造函数
默认构造函数是可以无参调用的构造函数。
- 既可以是定义为空参数列表的构造函数
- 也可以是所有参数都有默认参数值的构造函数。
class Circle1
{
private:
double radius;
public:
Circle1()
{ // 无参数
radius = 1.0; /*函数体可为空*/
}
};
class Circle2
{
private:
double radius;
public:
Circle2(double r = 1.0) // 所有参数都有默认值
: radius{r}
{
}
};
若对象类型成员/内嵌对象成员没有被显式初始化
(1) 该内嵌对象的无参构造函数会被自动调用;(2) 若内嵌对象没有无参构造函数,则编译器报错
class X
{
private:
Circle c1;
public:
X() {}
};
class X
{
private:
Circle c1;
public:
//手动调用默认构造函数
X() c1{}
{
}
};
若类的数据域是一个对象类型(且它没有无参构造函数),则该对象初始化可以放到构造函数初始化列表中
成员的初始化次序
- Default Member Initialization (就地初始化) ;
- Constructor Initialization List (构造函数初始化列表)
- Assign Values to the members in Ctor Body (在构造函数体中为成员赋值)。注意,这个不是初始化,而是赋值。
哪个起作用(初始化/赋值优先级): 在Ctor 函数体中为成员赋值 > Ctor 初始化列表 > 就地初始化
若一个成员同时有就地初始化和构造函数列表初始化,则就地初始化语句被忽略不执行
#include <iostream>
int x = 0;
struct S
{
int n = ++x; // default initializer
S() {} // 使用就地初始化(default initializer)
S(int arg) : n(arg) {} // 使用成员初始化列表
};
int main()
{
std::cout << x << '\n'; // 输出 0
S s1;
std::cout << x << '\n'; // 输出 1 (default initializer ran)
S s2(7);
std::cout << x << '\n'; // 输出 1 (default initializer did not run)
system("pause");
}
Delegation Constructor (代理构造)
含义: One ctor can call another ctor (一个构造函数可以调用另外的构造函数,C++11)
//A() -> A(int) -> A(int, int)
class A
{
public:
A() : A(0) {}
A(int i) : A(i, 0) {}
A(int i, int j)
{
num1 = i;
num2 = j;
average = (num1 + num2) / 2;
}
private:
int num1;
int num2;
int average;
};
避免递归调用目标ctor
//A() -> A(int) -> A(int, int) -> A()
class A
{
public:
A() : A(0) {}
A(int i) : A(i, 0) {}
A(int i, int j) : A() {}
private:
int num1;
int num2;
int average;
};
Copy Constructor(拷贝构造函数 , copy ctor / cp ctor / cp)
拷贝构造:用一个对象初始化另一个同类对象
Circle(Circle &);
Circle(const Circle &);
class X
{ //来自C++11标准: 12.8节
// ...
public:
X(const X &, int = 1);
};
X b(a, 0); // calls X(const X&, int);
X c = b; // calls X(const X&, int);
调用拷贝ctor
//只有在对象定义时,称为拷贝构造函数;其他时候是赋值。
Circle c1( 5.0 );
Circle c2( c1 ); //c++03
Circle c3 = c1; //c++03
Circle c4 = { c1 }; //c++11
Circle c5{ c1 }; //c++11
隐式声明的拷贝构造函数
一般情况下,如果程序员不编写拷贝构造函数,那么编译器会自动生成一个,自动生成的拷贝构造函数叫做“隐式声明/定义的拷贝构造函数,隐式声明的copy ctor简单地将作为参数的对象中的每个数据域复制到新对象中。
Shallow copy(浅拷贝)
——数据域是一个指针,只拷指针的地址,而非指针指向的内容
- Implicit/default copy ctor (创建新对象时,调用类的隐式/默认构造函数)
- default assignment operator for copying = (为已有对象赋值时,使用默认赋值运算符)
![](https://i-blog.csdnimg.cn/blog_migrate/17b5a94ff37be34a62b090148f668538.png)
Employee e1{"Jack", Date(1999, 5, 3), Gender::male};
Employee e2{"Anna", Date(2000, 11, 8), Gender : female};
Employee e3{e1}; //cp ctor,执行一对一成员拷贝
![](https://i-blog.csdnimg.cn/blog_migrate/3e87fc0a02d6398fc92c9622765185f9.png)
Deep copy(深拷贝:拷贝指针指向的内容)
class Employee
{
public:
// Employee(const Employee &e) = default; //浅拷贝ctor
Employee(const Employee &e)
{ //深拷贝ctor
birthdate = new Date{e.birthdate};
} // ...
};
Employee e1{"Jack", Date(1999, 5, 3), Gender::male};
Employee e2{"Anna", Date(2000, 11, 8), , Gender : female};
Employee e3{e1}; //cp ctor 深拷贝
形象解释(深浅拷贝):
前提条件:类A中有个指针 p,指向一个外挂对象b(b是B类型的对象);如果类A里面没有指针成员 p,那也就不要谈深浅拷贝了。
现在有一个类A的对象a1(a1的指针p指向外挂对象b1)。以拷贝构造的方式,创建a1的一个拷贝a2。
- 如果仅仅将a1.p的值(这个值是个地址)拷贝给 a2.p,这就是浅拷贝。浅拷贝之后,a1.p和a2.p都指向外挂对象 b1
- 如果创建一个外挂对象b2,将 a2.p指向b2;并且将b1的值拷贝给b2,这就是深拷贝
Destructor(析构函数)
Destructor | Constructor | |
When to invoke(何时调用) | when the object is destroyed(对象销毁时) | when an object is created(对象创建时) |
Prototype(原型) | C::~C( ) | C::C(arguments) |
Default prototype(默认函数的原型) | C::~C( ) | C::C( ) 或参数带有默认值 |
What if no explicit decl? (没有显式声明怎么办) | Compiler will create a default one (编译器会生成默认函数) | |
Overloadable(可否重载) | No, only 1 | Yes |
成员拷贝
Example: circle2 = circle1 ;
将circle1 的数据成员拷贝到circle2 中,函数没有拷贝,拷贝后,circle1 和circle2是两个不同的对象。
匿名对象(Anonymous Object)
int main()
{
Circle C1 = Circle{1.1};
auto C2 = Circle{2.2}; //用匿名对象做拷贝列表初始化
Circle c3{}; //直接列表初始化,调默认Ctor
c3 = Circle{3.3}; //用匿名对象赋值.
cout << "Area is " << Circle{4.2}.getArea() << endl;
cout << "Area is" << Circle().getArea() << endl; // 不推荐
cout << "Area is" << Circle(5).getArea() << endl; //不推荐
return 0;
}
一个简单的类Circle
Circle.h
//#ifndef CIRCLE_H_//#define CIRCLE_H_
_Pragma("once") class Circle
{private:
double radius;
public:
Circle(/* args */);
Circle(double radius_);
double getArea();
double getRadius() const;
void setRadius (double radius);
};
//#endif
Circle.cpp
#include <iostream>
#include "Circle.h"
Circle::Circle()
{
radius = 1.0;
}
Circle::Circle(double radius_)
{
radius = radius_;
}double Circle::getArea()
{
return (3.14 * radius * radius);
}double Circle::getRadius() const
{
return radius;
}void Circle::setRadius (double radius)
{
this->radius=radius;
}
main.cpp
#include <iostream>
#include "Circle.h"
#include "Circle.cpp"int main()
{
Circle c1;
Circle c2{2.0};
std::cout << c1.getArea() << std::endl;
std::cout << c2.getArea() << std::endl;
Circle ca1[]
{
Circle{1.0}, Circle{2.0}, Circle { 3.0 }
} ;
Circle ca2[]{10.0, 11.0, 12.0} ;
ca1[2].setRadius(4.0);
ca2[1].setRadius(100.0);
for (int i = 0; i < static_cast<int>(sizeof(ca1) / sizeof(ca1[0])); i++)
{
std::cout << ca1[i].getArea() << std::endl;
}
for (auto x : ca2)
{
std::cout << x.getArea() << std::endl;
}
system("pause");
return (0);
}
![](https://i-blog.csdnimg.cn/blog_migrate/d8d3abc3c6681e26bc4cb55ddcd1bf09.png)
避免头文件被多次包含
问题引入
1. //a.h
2. void f();
3.
4. // a.cpp
5. #include "a.h"
6. void f() {}
7.
8. //main.cpp
9. #include "a.h"
10. void f() {}
第6行和第10行构成了语法错误:重定义函数
第一种方法:
#ifdef
#define
#endif
第二种方法:
#pragma once // C++03, C90
第三种方法:
_Pragma("once") //C++11, C99;
__pragma(once)//微软版本
_Pragma实际上是一个运算符(operator)
在堆中创建对象
ClassName *pObject = new ClassName {} ; //用无参构造函数创建对象
ClassName *pObject = new ClassName{arguments}; //用有 参构造函数创建对象delete pObject;
TestObjectPointer.cpp
#include <iostream>
#include "Circle.h"
#include "Circle.cpp"int main()
{
auto *c1 = new Circle(1.0);
Circle c3{2.0};
auto c2 = &c3;
std::cout << (*c1).getArea() << std::endl;
std::cout << c2->getArea() << std::endl;
auto c5 = new Circle[3]{1.0, 2.0, 3.0};
for (int i = 0; i < 3; i++)
{
std::cout << c5[i].getArea() << std::endl;
}
/*for(auto x:c5)
{
std::cout<<c5[x].getArea()<<std::endl;
}*/
//不能使用基于范围的for循环,范围必须是集合数据类型,比如数组或其他容器,c5是指针
delete c1;
delete[] c5;
c1 = c5 = nullptr;
delete c1;//删除空指针不会异常
system("pause");
return (0);
}
![](https://i-blog.csdnimg.cn/blog_migrate/2366af2c1a62c41bca028c56a506b4a2.png)
数据域封装
数据域采用public的形式有2个问题:
(1) First, data may be tampered. ( 数据会被类外的方法篡改)
(2) Second, it makes the class difficult to maintain and vulnerable to bugs. ( 使得类难于维护,易出现bug)
所以一般数据都保存在private中 ,用 类中函数getter (获取器)setter (设置器)去调用和改变。
Class abstraction (类抽象)
The process of removing physical, spatial, or temporal details or attributes in the study of objects or systems in order to more closely attend to other details of interest
( 在研究对象或系统时,为了更加专注于感兴趣的细节,去除对象或系统的物理或时空细节/ 属性的过程叫做抽象)
Class encapsulation (类封装)
A language mechanism for restricting direct access to some of the object's components.( 一种限制直接访问对象组成部分的语言机制)
A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data ( 一种实现数据和函数绑定的语言构造块)
friend(友元)
私有成员无法从类外访问,但有时又需要授权某些可信的函数和类访问这些私有成员,所以用friend关键字声明友元函数或者友元类。友元的缺点:打破了封装性。
#include <iostream>
#include <array>
using namespace std;
class Date
{
private:
int year{2019}, month{1};
int day{1};
public:
friend class Kid;
friend void print(const Date &d);
};
void print(const Date &d)//没有Date::
{
cout << d.year << "/" << d.month<< "/" << d.day << endl;
}
class Kid
{
private:
Date birthday;
public:
Kid()
{
cout << "I was born in "<< birthday.year << endl;
}
};
int main()
{
print(Date());
Kid k;
cin.get();
}
immutable object(不可变类)
——对象创建后,其内容不可改变,除非通过成员拷贝
- Mark all data fields private (所有数据域均设置为“私有”属性)
- No mutator functions (没有更改器函数)
- No accessor that would return a reference/pointer to a mutable data field object(也没有能够返回可变数据域对象的引用或指针的访问器)
![](https://i-blog.csdnimg.cn/blog_migrate/eb21896e83f9d20125e307d7be6f2d5f.png)
int main()
{
Employee empJack("Jack", Date(1970, 5, 3), Gender::male);
Date *birthday = empJack.getBirthday();//返回的是Date对象的指针
birthday->setYear(2010);//值被改变
cout << "birth year after the change is " << empJack.getBirthDate()->getYear() << endl;
return 0;
}
数据成员的作用域
The data members are accessible to all constructors and functions in the class. (数据成员可被类内所有函数访问)
Data fields and functions can be declared in any order in a class. (数据域与函数可按任意顺序声明)
If a local variable has the same name as a data field: (若成员函数中的局部变量与某数据域同名)
(1) the local variable takes precedence ( 局部变量优先级高:就近原则)
(2) the data field with the same name is hidden. ( 同名数据域在函数中被屏蔽)
为避免混淆,不要在类中多次声明同名变量,除了函数参数
The this Pointer (this指针)
this 可以访问函数内类中被屏蔽的数据域,this指向当前类实例对象
This 关键字的特性
(1) a special built-in pointer ( 特殊的内建指针)
(2) references to the calling object. ( 引用当前函数的调用对象)
编码规范
If the parameter of a member function has the same name as a private class variable, then the parameter should have underscore suffix.(若类的成员函数参数与私有成员变量名相同,那么参数名应加下划线后缀)
类成员的就地初始化
In C++03, only static const members of integral types could be initialized in-class (在C++03标准中,只有静态常量整型成员才能在类中就地初始化)
class X {
static const int a = 7; // ok
const int b = 7; // 错误: 非 static
static int c = 7; // 错误: 非 const
static const string d = "odd"; // 错误: 非整型
// ...
};
C++11标准——非静态成员可以在它声明的时候初始化
class S {
int m = 7; // ok, copy-initializes m
int n(7); // 错误:不允许用小括号初始化
std::string s{'a', 'b', 'c'}; // ok, direct list-initializes s
std::string t{"Constructor run"}; // ok
int a[] = {1,2,3}; // 错误:数组类型成员不能自动推断大小
int b[3] = {1,2,3}; // ok
// 引用类型的成员有一些额外限制、参考标准
public:
S() { }
};
Constant expressions (常量表达式)—编译期可以计算值的一个表达式
const vs constexpr:
- const 修饰的对象未必是编译期常量,const 是用来告知程序员,const 修饰的内容是不会被修改的。主要目的是帮程序员避免bug 。
- constexpr 用在所有被要求使用“constant expression”的地方(就是constexpr 修饰的东西可以在编译期计算得到值),主要目的是让编译器能够优化代码提升性能 。
#include <iostream>
#include <array>
using namespace std;
constexpr int max(int a, int b)
{ // c++11 引入 constexpr
if (a > b)
return a; // c++14才允许constexpr函数中有分支循环等
else
return b;
}
int main()
{
int m = 1;
const int rcm = m++; // rcm是运行期常量
const int cm = 4; // 编译期常量,等价于: constexpr int cm = 4;
int a1[max(m, rcm)]; // 错误:m & rcm 不是编译期常量
std::array<char, max(cm, 5)> a2; // OK: cm 和 5 是编译期常量
system("pause");
}
编译期常量
编译时,所有编译期常量都将被替换成字面量,类型必须是基本类型或String。
运行时常量
在运行时才能确定它的值。
关于编译期间在类中定义常量,用下面的一个问题来说明。
问题:在类中建立一个int类型的数组
方法一:(错误)
class Test
{
const int size = 100;
int array[size];
//……
};
错误原因:
- 因为在一个类中,const 恢复了它在 c 中的一部分意思,在每个类对象里分配存储并代表一个值,这个值一旦被初始化以后就不能被改变。所以在类中使用了const的意思是:在这个对象的生命周期内,它是一个常量。 然而,每个对象可能包含不同的值。
- 对const常量进行了初始化,C++中这个初始化必须由构造函数完成,如const常量在初始化列表中进行初始化。
方法二:(正确,有缺陷)
使用enum;
class Test
{
enum { size = 100};
int array[size];
//……
};
使用enum不会占用对象中的存储空间的,枚举常量在编译的时候被全部求值。
缺点:
假如定义一个非整型的常量该如何?enum无法完成此项操作,同时丧失了枚举本来的作用。
方法三:(正确,最好)
使用静态常量;
class Test
{
static const int size;
int array[size];
//……
};
const int Test::size = 100;
它既是常量,不能改变,又是静态,在类中只有一个定义点。所以能够完成任务。
同时,它可以定义任何与定义类型的常量。
assert and static_assert(C++11)
assert : C语言的宏(Macro),运行时检测。
用法:包含头文件 <cassert> 以调试模式编译程序。
assert( bool_expr ); // bool_expr 为假则中断程序
NDEBUG这个宏是C/C++标准规定的,所有编译器都有对它的支持。
- 调试(Debug)模式编译时,编译器不会定义NDEBUG,所以assert()宏起作用。
- 发行(Release)模式编译时,编译器自动定义宏NDEBUG,使assert()不起作用。
如果要强制使得assert()生效或者使得assert()不生效,只要手动 #define NDEBUG 或者 #undef NDEBUG即可。
assert 帮助调试解决逻辑bug (部分替代“断点/单步调试”),比如下面这个例子,在 “i>0” 时程序能运行,在 “i<=0” 时程序直接停止。
#include <cassert>
#include <iostream>
#undef NDEBUG // 强制以debug模式使用<cassert>
using namespace std;
int main()
{
int i;
std::cout << "Enter an int: ";
std::cin >> i;
assert((i > 0) && "i must be positive");
system("pause");
return 0;
}
static_assert ( bool_constexpr, message) ——(C++11的静态断言 )
- bool_constexpr: 编译期常量表达式,可转换为bool 类型,不能有变量表达式。
- message: 字符串字面量 ,是断言失败时显示的警告信息。自C++17起,message是可选的。
作用:编译时断言检查(常用在模版编程中 ,对写库的作者用处大。)
// 下面的语句能够确保该程序在32位的平台上编译进行。
// 如果该程序在64位平台上编译,就会报错 (例子来自MSDN)
static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");
Static Members(静态成员)
- 声明为“constexpr”类型的静态数据成员必须 在类中声明 并初始化。自C++17 起,可不在类外定义
- 声明为“inline”(C++17 起) 或者 “const int” 类型的静态数据成员可以 在类中声明 并初始化;
- 静态数据成员具有静态存储期(static storage duration)或者C++11线程存储期特性( 对象的存储在程序开始时分配,而在程序结束时解回收)
- Only one instance of the object exists ( 只存在对象的一个实例)
- 静态存储器对象未明确初始化时会被自动“零初始化(Zero-Initialization)”
- 静态函数内部不能有非静态的成员
class A
{
public:
A(int a = 0)
{
x = a;
}
static void f1()
{
y++;//若为 x++ 则有误,静态函数内部不能有非静态的成员
};
private:
int x;
static int y;
};
- 其它须在类外 定义并初始化,且不带static 关键字
#include <iostream>
using namespace std;
class Square
{
private:
double side;
static int numberOfObjects;
// ...
public:
Square() : Square(1.0)
{
}
Square(double side)
{
this->side = side;
numberOfObjects++;
}
// ...
};
int Square::numberOfObjects;//类外初始化
int main()
{
Square s1{}, s2{5.0};
system("pause");
}
单件模式(Singleton Pattern)
确保一个类只有一个实例,并提供一个全局访问点
class Date
{
private:
Date(int, int, int);
Date();
public:
void addDay(int);
string tostring(char);
static Date *getInstance(); //重点
bool setYear(int year.);
int getYear() const;
bool setMon(int month, );
int getMon() const;
bool setDay(int day_);
int getDay() const;
private:
int year;
int month;
int day;
static const int maxDay[2][13];
static Date *mpDafe; //重点
};
Date *Date::mpDate = nullptr;
Date *Date ::getInstance()
{
if (mpDate == nullptr)
{
mpDate = new Date();
return mpDate;
}
else
return mpDate;
}
cout << Date::getInstance()->toString('/') << endl;