Inside Object
在类中,有几个基础知识点:
- 访问控制 Access Control
- 对象生命 Objects in different places
- 静态内容 Static
- 引用 Reference
- 常量 Const
- 空间分配 Operator
new
anddelete
访问控制
权限关键字
在类中,成员的访问权限分为三种:public
、private
和 protected
。这决定了在不同的地方能否访问成员。
public
:类内外的所有地方都可以访问。private
:只有类内可以访问。protected
:只有类内、该类的派生类内可以访问。派生类在 Lec7 继承 中会介绍。
注意,权限的管理是针对类的,同类的不同对象在其成员函数内可以相互访问任意成员。
友元
友元像一个通行证,精确地告诉编译器某个函数、某个类可以任意访问本类的成员。
如果说 B
是 A
的友元,那么 B
中可以访问 A
的所有成员。
友元关系没有对称性,如果 B
是 A
的友元, A
未必是 B
的友元。
友元关系没有传递性,即如果 B
是 A
的友元,C
是 B
的友元,不代表 C
可以访问 A
的所有成员。
友元类
使用 friend
语句可以使其他类成为本类的友元。
class A{
public:
A():x(0){}
~A(){std::cout << x << std::endl;}
private:
int x;
friend class B;
};
class B{
public:
void add(A& a){a.x++;}
};
int main(){
A a; B b;
b.add(a);
return 0;
}
编译通过,运行发现析构时输出了 1
。
友元函数
声明友元函数,同样是使用 friend
语句。
class A{
public:
A():x(0){}
~A(){std::cout << x << std::endl;}
private:
int x;
friend void add(A& a);
};
void add(A& a){a.x++;}
int main(){
A a;
add(a);
return 0;
}
结果同上。注意这里的 add(A)
是全局函数。
对象生命
变量生命周期
在类中,生命周期有三种:
- 字段 Fields:成员变量,在类内定义,作用域是整个类。它们的生命周期等于对象的生命周期,对象被销毁时它们就销毁。
- 参数 Parameters:函数参数,生命周期只在函数内。
- 本地变量 Local Variables:和普通的本地变量一样,生命周期从声明处到作用域的末尾。
声明顺序
#include "X.h"
X global_x1(12, 34);
X global_x2(8, 16);
构造函数会在 main()
之前调用(这下不是最早的了),先声明的先调用(global_x1
比 global_x2
早)。
在 main()
返回或者 exit()
被调用时,开始调用析构函数。析构函数按照相反的顺序调用。(先构造的后析构)。
但是,对于多文件之间的先后顺序,并没有规定。
以下三种情况称为非局部的静态对象(non-local static):
- 定义为全局的
- 定义为类内静态的
- 定义为文件内静态的
非局部静态对象如果产生声明依赖,可能引发未知的错误。
如何解决?尽量避免这样的依赖;或者将静态对象按顺序放进同一个文件。
静态内容
静态有两层含义:静态的空间和受限的访问。
在 C++ 中,不要在文件作用域中使用静态变量,只应该在函数作用域或者类的作用域中使用。
静态局部变量
用于函数内部,不随着函数返回而销毁。只有在第一次遇到声明语句的时候初始化,其他时候继承上次调用后的值。
class X{
public:
X(int, int);
~X();
// ...
};
void f(int x){
if(x > 10){
static X my_X(x, x * 21);
// ...
}
}
本例中,当且仅当传参有 x > 10
时,my_X
会被初始化。如果 my_X
没有被初始化,那么就不会执行析构。
静态成员变量
类内的静态成员变量,是独立于任一对象的属性。这个类的所有对象,都可以访问这个静态成员变量。
当然,因为静态成员变量不属于任何对象,所以需要单独的语句声明其空间。
在类内声明静态成员变量后,单独声明空间时不需要加 static
。下面是一个 mem.h
文件和 mem.cpp
文件。
#ifndef HEADER_H_
#define HEADER_H_
class Mem{
public:
static int cnt;
Mem();
~Mem();
private:
int x;
};
#endif
#include "mem.h"
int Mem::cnt;
Mem::Mem(): x(0){
cnt++;
}
Mem::~Mem(){
}
静态成员函数
和静态成员变量类似,独立于任一对象。
因为不属于任一对象,所以它没有隐藏的 this
指针。
在实现时不需要写 static
。同时,它也不能被动态绑定地重载(见第 7 节)。
访问方法
有两种方法可以访问静态内容:
<class name>::<static member>
<object name>.<static member>
引用
左值和右值
先复习一下表达式中的左值和右值。
左值,通俗说就是可以出现在赋值号左边的值;右值,自然就是可以出现在赋值号右边的值。当然,左值也可以出现在右边,所以右值的定义要修改为只能出现在右边。
实际上,左值指表达式完成后仍然存在的对象,右值指表达式完成后不再存在的对象。
只有变量名、引用、以及 *
、[]
、.
、->
的结果可以作为左值。
特例:字符字面量是不可算入右值的字面量,因为它实际上存储在静态内存区,一直存在。
左值引用
左值引用,就是对左值的引用。它像给变量起了一个别名,两个变量名访问的是同一个地址。
int a = 1;
int &b = a;
b++;
std::cout << a << '\n';
std::cout << &a << ' ' << &b << '\n';
会发现 a
也变成了 2,&a
和 &b
的输出一样。
当然,左值引用不能绑定右值,因为右值并没有持久的地址。
特例:常量左值引用可以绑定右值。这时引用会分配一个新的地址(相当于新变量)。
函数参数中也可以使用引用,称为传址调用:
void swap(int& x, int& y){
int tmp = x;
x = y;
y = tmp;
}
// in main()
int a = 1, b = 2;
swap(a, b);
这样就可以不写指针而修改参数。
几条规则:
- 不允许定义引用的引用
- 不允许定义引用的指针
- 允许定义指针的引用(给指针起别名)
int *&p = q;
- 不允许引用的数组
右值引用
右值引用和左值引用职能刚好相反,不能绑定左值、只能绑定右值。
右值引用在初始化之后就有了地址,相当于一个新变量。
在重载构造函数和赋值运算的时候,右值引用有更多的用途。
int x = 20;
int &&rx = x << 1; // Ok: 右值(40)
int y = rx + 2;
rx = 100; // Ok: 右值(100)
int &&rrx1 = x; // Error: 不能绑定左值
const int &&rrx2 = x; // Error: 同上
void fun(int& lref){
std::cout << "l-value" << std::endl;
}
void fun(int&& rref){
std::cout << "r-value" << std::endl;
}
int main(){
int x = 10;
fun(x); // l-reference
fun(10); // r-reference
}
透彻理解C++11 移动语义:右值、右值引用、std::move、std::forward - KillerAery - 博客园 (cnblogs.com)
常量
顾名思义,就是指定义之后不能修改的量。
在 C++ 中,编译器会避免创建常量,而是将常量的值记录在符号表里,优化时间。
与之相反地,extern
关键字会强制检查变量的空间。
传参时,总是可以把非常量当作常量;但不能把常量当作左值使用,除非使用关键字 const_cast
。
编译期常量
如果你给某个全局常量写入了一个确定的值:
const int bufsize = 1 << 10;
那么在编译期编译器就知道了这是个常数,会尝试替换。
一个简单的例子:声明数组时,长度必须为常数。
const int bufsize = 1 << 10;
const int index[] = {1, 2, 3, 4};
int f[bufsize]; // Ok: f[1024]
int f[index[3]]; // Error
常量和指针
指针的特殊性,使得指针常量有两种属性:是否能移动指向的位置、是否能修改指向的对象的内容。
诀窍: *
后的 const
,表示不能移动指向的位置;*
前的 const
,表示不能修改指向的对象的内容。
std::string str("Fred");
const std::string* p1 = &str;
std::string const* p2 = &str;
std::string* const p3 = &str;
此例中,p1
和 p2
是一样的,不能修改 str
的内容,而 p3
可以。
注意字符指针和字符数组的区别:
- 字符指针指向的是常字面量,不能修改
- 字符数组是一个数组,可以修改
char *s = "Hello World!"; // 可以移动,不能修改
char s[] = "Hello World!"; // 不能移动,可以修改
常量对象
声明为常量的对象,只能使用声明为常量的成员函数,这些函数内部不能修改成员变量。
class Day{
public:
Day();
Day(int _y, int _m, int _d);
~Day();
void setday(int _d);
int getday()const;
private:
int year, month, day;
};
Day::Day(){}
Day::Day(int _y, int _m, int _d): year(_y), month(_m), day(_d){}
Day::~Day(){}
void Day::setday(int _d){this->day = _d;}
int Day::getday()const{return this->day;}
那么对于常量 const Day cur(2024, 08, 14);
而言,只能调用 getday()
而不能调用 setday()
。
当然,非常量的对象也可以用 getday()
;实际上,可以重载 getday()
和 getday()const
,使得常量和非常量调用 getday()
得到不同的结果。
注意:对象的常量不是编译期常量,所以下面的例子不能通过编译:
class Array{
const int size = 10;
int array[size];
};
解决方法有两种,使用 enum
或者 static
:
enum {size = 10};
static const int size = 10;
空间分配
在 C 语言中,我们使用 malloc()
和 free()
函数来动态分配空间。在 C++ 中,我们会使用更加方便的 new
和 delete
关键字来完成这个任务。
基本语法
new
new
用于分配一个新的空间,同时执行构造函数。
Node *p1 = new Node;
Node *p2 = new Node(1, 2);
Node *p3 = new Node[10];
不指定时,将会使用默认构造函数。
delete
delete
用于释放 new
分配的空间,会执行析构函数。
当释放的对象是数组时,使用 delete[] p
(不管 p
实际的维数)。
delete p1; delete p2; delete[] p3;
要点
- 对空地址
nullptr
使用delete
是安全的。 - 对不是
new
分配的空间使用delete
会引发错误。 delete
之后的指针不能再delete
一遍,会引发错误。- 注意配套使用方括号。