面向对象基础(二)
对象指针
用指针访问对象成员
-
对象指针可以指向新的对象名。
-
箭头运算符
->
:用指针访问对象成员
实例:
Circle circle1;
Circle* pCircle = &circle1;
cout << "The radius is " << (*pCircle).radius << endl;
cout << "The area is " << (*pCircle).getArea() << endl;
(*pCircle).radius = 5.5;
cout << "The radius is " << pCircle->radius << endl;
cout << "The area is " << pCircle->getArea() << endl;
在堆中创建对象
在函数中声明的对象都在栈
上创建。当函数返回,则对象被销毁。(类似于局部变量)
为保留对象,你可以用new
运算符在堆上创建它。
实例:
Circle *pCircle1 = new Circle{}; //用无参构造函数创建对象
Circle *pCircle2 = new Circle{5.9}; //用有参构造函数创建对象
//程序结束时,动态对象会被销毁,或者
delete pCircle1; //用delete显式销毁
对象数组
4种对象数组声明方式
- 声明方式1
Circle ca1[10];
- 声明方式2,用匿名对象构成的列表初始化数组
Circle ca2[3] = {
Circle{3},
Circle{ },
Circle{5}
};
// 注意:不可以写成: auto ca2[3]=
// 因为声明数组时不能用auto
- 声明方式3,用C++11列表初始化,列表成员为隐式构造的匿名对象
Circle ca3[3] { 3.1, {}, 5 };
Circle ca4[3] = { 3.1, {}, 5 };
- 声明方式4,用new在堆区生成对象数组,这需要在适当位置delete
auto* p1 = new Circle[3];
auto p2 = new Circle[3]{ 3.1, {}, 5 };
delete [] p1;
delete [] p2;
p1 = p2 = nullptr;
对象与函数传参
对象作为函数参数
对象作为函数参数,可以按值传递也可以按引用传递,也可以按指针传递。
值传递
// Pass by value
void print( Circle c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( myCircle );
/* … */
}
引用传递
void print( Circle& c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( myCircle );
/* … */
}
指针传递
// Pass by pointer
void print( Circle* c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( &myCircle );
/* … */
}
对象作为函数返回值
返回对象
// class Object { ... };
Object f ( /*函数形参*/ ){
// Do something
return Object(args);
}
int main() {
Object o = f ( /*实参*/ );
f( /*实参*/ ).memberFunction();
}
返回对象指针
// class Object { ... };
Object* f ( /*函数形参*/ ){
Object* o = new Object(args) // 这是“邪恶”的用法,不要这样做
// Do something
return o;
}
int main() {
Object* o = f ( /*实参*/ );
f( /*实参*/ )->memberFunction();
}
// 记得要delete o
在函数体里new对象,却要在函数体外delete对象,是一个“邪恶”的用法,因为如果别人没有仔细看你的代码,但要用你的函数,他不会知道要在函数体外delete一个对象。
较好的用法:
// class Object { ... };
Object* f ( Object* p, /*其它形参*/ ){
// Do something
return p;
}
int main() {
Object* o = f ( /*实参*/ );
}
// 没有 delete o 这件事
尽可能用
const
修饰函数返回值类型和参数除非你有特别的目的(使用移动语义等)。
const Object f(const Object p, /* 其它参数 */) { }
返回对象引用
// class Object { ... };
Object& f ( /*函数形参*/ ){
Object o {args};
// Do something
return o; //这是邪恶的用法
}
o 局部变量,生存期只在这个函数内,却返回了它的引用,这是一个“邪恶”的用法。
- 可行的用法1:
// class Object { ... };
class X {
Object o;
Object f( /*实参*/ ){
// Do something
return o;
}
}
- 可行的用法2(常见):
// class Object { ... };
Object& f ( Object& p, /*其它形参*/ ){
// Do something
return p;
}
// main() {
auto& o = f ( /*实参*/ );
f( /*实参*/ ).memberFunction();
用
const
修饰引用类型的函数返回值,除非你有特别目的(比如使用移动语义)
const Object& f( /* args */) { }
一些高阶问题
传值,传址,传指针,传引用都是骗初学者的。C++中有意义的概念是传值和传引用
-
Differences between a pointer variable and a reference variable
https://stackoverflow.com/a/57492 -
Difference between passing by reference vs. passing by value?
https://stackoverflow.com/a/430958
何时引用,何时指针?
一般来说,能用引用尽量不用指针。引用更加直观,更少出现意外的疏忽导致的错误。
指针可以有二重、三重之分,比引用更加灵活。有些情况下,例如使用 new 运算符,只能用指针。
关于指针与引用的区别,可以看 优快云 的**【这篇文章】**,讲得很细致;在该文中的第5部分,也讲了函数传参时“指针传递”与“引用传递”的差别,但这个解释比较晦涩,需要你有汇编语言或者微机原理或者计算机组成原理方面的知识方能透彻理解。在《深入探索C++对象模型》这本书中也有关与引用的解释。
抽象与封装
数据域封装
数据域采用public的形式有2个问题:
- 数据会被类外的方法篡改
- 使得类难于维护,易出现bug
class Circle {
public:
double radius;
//……
};
// main() {
circle1.radius=5; //类外代码可修改public数据
所以,应将类外部不需要直接访问的数据域放在private
下
访问器(getter)与更改器(setter)
如果确实要在类外部访问和更改私有数据,可以设置get
/set
函数
- getter一般原型
returnType getPropertyName() const{
return PropertyName;
}
- setter一般原型
void setPropertyName(dataType PropertyValue){
this->PropertyName = PropertyValue;
}
getter和setter可以借助集成开发环境(IDE)一键生成。
类抽象与封装(概念)
抽象
在研究对象或系统时,为了更加专注于感兴趣的细节,去除对象或系统的物理或时空细节/ 属性的过程叫做抽象。
封装
一种限制直接访问对象组成部分的语言机制。
一种实现数据和函数绑定的语言构造块。
总结
-
抽象
提炼目标系统中我们关心的核心要素的过程
-
封装:
绑定数据和函数的语言构造块,以及限制访问目标对象的内容的手段
实例
- 抽象
实际的圆有大小、颜色;数学上的圆有半径(radius)、面积(area)等。
抽象的过程是,将我们的关注的东西提取出来,比如:“给定半径r,求面积”
- 封装
我们要限制对radius的访问, 然后用“class”把数据和函数绑定在一起。
成员作用域与this指针
数据成员的作用域
数据成员可被类内所有函数访问。
数据域与函数可按任意顺序声明。
同名屏蔽
若成员函数中的局部变量与某数据域同名:
- 局部变量优先级高:就近原则
- 同名数据域在函数中被屏蔽
为避免混淆,不要在类中多次声明同名变量,除了函数参数
this指针
如果真的有一个函数的参数与数据域的变量重名,却依然想访问类中被屏蔽的数据域?
可以使用this
关键字。
this
关键字的特性:
- 特殊的内建指针,即不需程序员声明,一旦对象创建了,就存在。
- 引用当前函数的调用对象。
this
指针具体代表的是当前函数所调用对象的指针。
实例
class Circle {
public:
Circle();
Circle(double radius)
{
this->radius = radius;
}
private:
double radius;
public:
void setRadius(double radius_) const{
radius = radius_;
}
double getArea(){
return 3.14 * radius * radius;
}
};
这段代码,说明了:
-
数据成员可被类内所有函数访问
例如函数
double getArea();
里访问了radius
属性。 -
数据域与函数可按任意顺序声明
例如私有数据域里的属性
radius
,夹在函数中间。 -
使用this指针访问类中被屏蔽的数据域
例如在有参构造函数中,其参数radius与数据域中的属性同名。
这时,可以使用
this->radius
访问类的数据域中的radius
。
编码规范:
If the parameter of a member function has the same name as a private class variable, then the parameter should have underscore suffix.
若类的成员函数参数与私有成员变量名相同,那么参数名应加下划线后缀。
比如,
setRadius
函数的参数radius_
由radius
和下划线构成,避免了和私有成员变量名相同。这比使用this指针要清晰的多。
类的初始化
[C++11]类成员的就地初始化
在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标准中,非静态成员可以在它声明的时候初始化。
“就地初始化”的术语的来源有多处:
-
就地初始化:《深入理解C++11》
-
In-class initializer : https://isocpp.org/
-
default member initializer : https://cppreference.com
实例
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() { }
};
要注意的是:
int n(7);
这种初始化方式不允许出现在类的声明中。因为这会和函数调用混淆。int a[] = {1,2,3};
在类中的数组类型成员不能自动推断大小
构造函数初始化列表
在构造函数中用初始化列表初始化数据域:
ClassName (parameterList)
: dataField1{value1}, dataField2{value2}
{
// Something to do
}
为何需要构造函数初始化列表?
- 类的数据域是一个对象类型,被称为对象中的对象,或者内嵌对象
- 内嵌对象必须在被嵌对象的构造函数体执行前就构造完成
class Time { /* Code omitted */ }
class Action {
public:
Action(int hour, int minute, int second) {
time = Time(hour, minute, second); //time对象应该在构造函数体之前构造完成
}
private:
Time time;
};
Action a(11, 59, 30);
也就是说,如果一个类里面内嵌了一个对象,那么这个对象必须要先于当前构造函数体执行之前就先构造。在代码里面,只有参数列表小括号后面,函数体大括号之前这个位置。
所以上述代码应该写成:
Action(int hour, int minute, int second)
: time{hour,mintue,second}
{
// Do Something Here
}
冒号这一行,就被称为初始化列表(Initializer Lists),要注意和列表初始化区分。
而有意思的是,这个初始化列表里,用了直接列表初始化的方式,读者应加以思索。
默认构造函数
默认构造函数是可以无参调用的构造函数。(既可以是定义为空参数列表的构造函数,也可以是所有参数都有默认参数值的构造函数)
实例:
class Circle1 {
public:
Circle1() { // 无参数
radius = 1.0; /*函数体可为空*/
}
private:
double radius;
};
class Circle2 {
public:
Circle2(double r = 1.0) // 所有参数都有默认值
: radius{r} {
}
private:
double radius;
};
参考:https://zh.cppreference.com/w/cpp/language/default_constructor
为什么需要默认构造函数?
-
若对象类型成员/内嵌对象没有被显式初始化
-
该内嵌内向的无参构造函数会被自动调用
class X { private: Circle c1; public: X() {} };
-
若内嵌对象没有无参构造函数,则编译器报错
-
-
当然,也可以在初始化列表中手工构造对象
class X { private: Circle c1; public: X(): c1{}{ } }
-
若类的数据域是一个对象类型(且它没有无参构造函数),则该对象初始化可以放到构造函数初始化列表中(也可以就地初始化)
成员的初始化次序
初始化次序
首先,需知晓初始化对象/类成员的三种方式:
- 就地初始化
- 构造函数初始化列表
- 在构造函数体中为成员赋值(其实这个不是初始化,是赋值)
执行次序:
就地初始化 --> Ctor
初始化列表 --> 在Ctor
函数体中为成员赋值
哪个起作用(初始化/赋值优先级):
在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 << std::endl;// 输出 0
S s1;
std::cout << x << std::endl;// 输出 1 (default initializer ran)
S s2{7};
std::cout << x << std::endl;// 输出 1 (default initializer did not run)
return 0;
}