OOP Lec4: Inside Object

Inside Object

在类中,有几个基础知识点:

  • 访问控制 Access Control
  • 对象生命 Objects in different places
  • 静态内容 Static
  • 引用 Reference
  • 常量 Const
  • 空间分配 Operator new and delete

访问控制

权限关键字

在类中,成员的访问权限分为三种:publicprivateprotected。这决定了在不同的地方能否访问成员。

  • public:类内外的所有地方都可以访问。
  • private:只有类内可以访问。
  • protected:只有类内、该类的派生类内可以访问。派生类在 Lec7 继承 中会介绍。

注意,权限的管理是针对的,同类的不同对象在其成员函数内可以相互访问任意成员。

友元

友元像一个通行证,精确地告诉编译器某个函数、某个类可以任意访问本类的成员。

如果说 BA 的友元,那么 B 中可以访问 A 的所有成员。

友元关系没有对称性,如果 BA 的友元, A 未必是 B 的友元。

友元关系没有传递性,即如果 BA 的友元,CB 的友元,不代表 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_x1global_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;

此例中,p1p2 是一样的,不能修改 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++ 中,我们会使用更加方便的 newdelete 关键字来完成这个任务。

基本语法

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 一遍,会引发错误。
  • 注意配套使用方括号。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇