slacr_

Just to record my life and thoughts.
笔记/编程/杂乱/极简

[C++笔记]类与对象

Apr 26, 2023C_C++6019 words in 40 min

C++笔记

类的访问控制

三种访问权限类型: public private protected
public 公共权限, 类内类外都能访问, 可继承
private 私有权限, 类内访问, 不可继承
protected 保护权限, 类内访问, 可继承
习惯将public写在类的最前.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
#include<windows.h>
using namespace std;
class Computer {
public:
Computer(){}
void turnON() {
power = true;
Sleep(1000);
cout << "loading 33%.." << endl;
Sleep(2000);
cout << "loading 70%.." << endl;
Sleep(1000);
cout << "loading 99%.." << endl;
cout << "welcome_" << endl;
}
private:
bool power;
protected:
string CPU;
};
int main() {
Computer c;
c.turnON();
return 0;
}

构造析构

构造函数在对象创建时自动调用初始化对象.
析构函数在对象生存期结束时自动调用, 完成内存回收工作.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Building {
public:
Building() { cout << "construct building" << endl; }
Building(int area) : area(area){} // 初始化列表
~Building() { cout << "destruct building" << endl; }
private:
int area;
};

int main() {
Building b;
return 0;
}

初始化列表与构造析构顺序

有些数据成员的初始化必须在初始化表中进行, 因为构造函数在在执行函数体前会先按初始化表初始化成员变量, 如果没有就是默认值, 有些情况就会报错. 一类是那些没有默认构造函数的内嵌对象, 因为这类对象初始化时必须提供参数,另一类是引用类型的数据成员, 因为引用型变量必须在初始化时绑定引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point {
public:
int x, y;
Point(){}
Point(int x, int y):x(x), y(y){
cout << "Point constructor" << "_" << x << endl;
}
Point(const Point& p) {
this->x = p.x;
this->y = p.y;
cout << "Point assignment" << "_" << x << endl;
}
~Point() {
cout << "Point destructor" << "_" << x << endl;
}
};

class Line {
public:
Point p1, p2;
Line() {}
Line(Point p1, Point p2) : p1(p1), p2(p2) {
cout << "Line constructor" << endl;
}
~Line() {
cout << "Line destructor" << endl;
}
};

int main() {
Point p1(1, 1);
Point p2(2, 2);
Line(p1, p2);
return 0;
}

构造一个类的对象时, 先构造内嵌对象再构造自身

构造与析构顺序相反, 因为函数调用栈的关系.

拷贝构造

把初始值对象的每个数据成员的值都复制到新建立的对象中。因此,也可以说是完成了同类对象的复制 (clone) • 这样得到的对象和原对象具有完全相同的数据成员,即完全相同的属性。

调用拷贝构造时机

  • 用类的一个对象去初始化类的另一个对象
  • 调用函数把对象当值传递时, 会用实参初始化形参.
  • 函数返回值为对象时, 会在主函数中创建一个生存期仅在调用语句中的临时无名对象, 用返回值的对象初始化该临时对象.

浅拷贝的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

class Test {
public:
int* p_num = NULL;
Test(){}
Test(int num){
p_num = new int(num);
}
~Test() {
delete p_num;
}
};

int main() {
Test t1(1);
Test t2(t1); // 或者 Test t2 = t1;
return 0;
}

手动释放指针的时候由于拷贝构造初始化的对象只是值传递, 造成析构时指针的重复释放.
可以通过重载拷贝构造解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test {
public:
int* p_num = NULL;
Test(){}
Test(int num){
p_num = new int(num);
}
// override copy constructor
Test(const Test& t) {
p_num = new int(*(t.p_num));
}
~Test() {
delete p_num; // 释放堆区空间
p_num = NULL; // 保险起见, 避免野指针.
}
};

int main() {
Test t1(1);
Test t2(t1);
return 0;
}

结构体

C++结构体是一种特殊形态的类, 结构体和类具有不同的默认访问控制属性, 类中默认访问控制权限是 private, 结构体中是 public.
结构体中, 习惯将数据成员设置为公共; 类中习惯设置为私有.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Node
{
public:
void gank() {

}
int foo;
float bar;
protected:
private:
};

struct Mode : public Node {

};

int main() {
Node N;
Mode M;
N = { 1 , 2.0 };
N.gank();
M.gank();
return 0;
}

联合体

联合体的全部数据成员共享同一组内存单元
联合体也可以不声明名称,称为元名联合体。无名联合体没有标记名,只是声明一个
成员项的集合,这些成员项具有相同的内存地址,可以自成员项的名字直接访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
public:
union {
int A, B, C;
};
Test(int A) {
}
};

int main() {
cout << sizeof(Test) << endl; // 4
return 0;
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union My {
int A, B, C;
};

class Test {
public:
My u;
Test(int A) {
}
};

int main() {
cout << sizeof(Test) << endl; // 4
return 0;
}

位域

某些数据只需要几个二进制位即可保存, 以通过冒号( :)后的位数来指定为一个位域所占用的二进制位数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Status { OK, WARNING, ERROR, UNSAFE};

class Feedback {
public:
Status s1 : 2;
Status s2 : 2;
};

int main() {
Feedback f;
cout << sizeof(Feedback) << endl; //4
f.s1 = OK;
cout << f.s1 << endl; // 0
return 0;
}

构造函数定义类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
class P {
public:
int x;
explicit P(int x) : x(x){}
};

class L {
public:
P p1, p2;
L(P p1, P p2) : p1(p1), p2(p2) {}
};

int main() {
L(P(1), P(2)); // 构造匿名对象, 也可以看成类型转换
L((P)1, P(2)); // 自定义类型转换
L(static_cast<P>(1), static_cast<P>(2)); // 和上面等同
L(1, 4); // 隐式转换, 参数列表比对时发生构造
// 给P 加上explicit 关键词后上, 不允许发生隐式类型转换P p1 = 1 这种情况实际上是P p1 = P(1).
return 0;
}

数据的保护和共享

作用域

作用域是一个标识符在程序正文中有效的区域。C++中标识符的作用域有函数原型作用域局部作用域(块作用域)类作用域命名空间作用域

命名空间

namespace , 为了区分不同的程序模块的标识符, 一个命名空间确定了一个命名空间作用域. 在命名空间内部, 可以直接引用当前命名空间中声明的标识符.

std是C++标准库的命名空间, 使用了该命名空间就不用再加std作用域了.
命名空间也允许嵌套.
具有命名空间作用域的变量也是全局变量.

两类特殊命名空间
全局命名空间: 所有显示声明的命名空间之外声明的标识符都定义在全局命名空间之下.
匿名命名空间: 不希望暴露给其他源文件使用, 仅在定义该匿名命名空间的编译单元中生效, 不需要使用声明.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
namespace 中文模式 {
class 字符串 {
public:
std::string s;
字符串(){}
字符串(const char* s):s(std::string(s)){

}
};
void 输出(字符串& s) {
std::cout << s.s << std::endl;
}

void 输出(const char* s) {
std::cout << s << std::endl;
}
void 输入(字符串& s) {
std::cin >> s.s;
}
字符串 环境 = "中文模式v0.1"; // 命名空间中的变量也是全局变量
}

using namespace 中文模式;
int main() {
输出(环境);
字符串 信息;
输出("请输入信息");
输入(信息);
输出(信息);
return 0;
}

可见性

从标识符引用的角度,来看标识符的有效范围,即标识符的可见性

对象生存期

对象从诞生到结束的时间段.

静态生存期

如果对象的生存期与程序的运行期相同,则称它具有静态生存期。在命名空间作用域中声明的对象都是具有静态生存期的。如果要在函数内部的局部作用域中声明具有静态生存期的对象,则要使用关键字 statìc
定义时未指定初值的基本类型静态生存期变量,会被赋予0值初始化,而对于动态生存期变量,不指定初值意味着初值不确定。

动态生存期

在局部作用域中声明的具有动态生存期的对象,习惯上也称为局部生存期对象。局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时。

静态成员变量

static关键字来声明静态成员, 类属性是描述类的所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的. 静态数据成员具有静态生存期。

静态数据成员不属于任何一个对象,因此可以通过类名对它进行访问,

在类的定义中仅仅对静态数据成员进行引用性声明,必须在命名空间作用域的某个地方使用类名限定定义性声明,这时也可以进行初始化.

静态成员函数

静态成员函数可以直接访问该类的静态数据和函数成员。而访问非静态成员,必须通过对象名。
静态成员可以通过类名访问.

友元

友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <cmath>
using namespace std;

class P {
friend double getLen(P p1, P p2); // 设置getLen为P友元, 可以访问P中所有成员
public:
P(){}
P(double x, double y) : x(x), y(y){}
private:
double x;
double y;
};

double getLen(P p1, P p2) {
return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}

int main() {
cout << getLen(P(0, 0), P(2, 2)) << endl;
return 0;
}

共享数据的保护

虽然数据隐藏保证了数据的安全性,但各种形式的数据共享却又不同程度地破坏了数据的安全。因此,对于既需要共享又需要防止改变的数据应该声明为常量。因为常量在程序运行期间是不可改变的,所以可以有效地保护数据。

常对象

常对象的数据成员值在对象的整个生存期间内不能被改变. 常对象必须在声明时初始化,因为之后不能更改。

1
2
3
4
5
6
7
8
9
10
11
12
class P {
public:
int x;
const int y;
P(){}
P(int x) : x(x){}
};

int main() {
const P p(3); // 常对象, 不可修改
P const p1(4); // 放在类型名后也可以
}

常数据成员

常数据成员只能被初始化,不能被赋值,因此要用初始化列表,或者定义时初始化.

常成员函数

成员函数有两类, 修改对象状态的成员函数,获取对象状态的成员函数。
常成员函数就是一种获取成员状态的函数,并且不能改变对象状态

  1. 常对象只能调用常成员函数
  2. 常成员函数只能调用常成员
  3. 常成员函数可以成为其它函数的重载
  4. 常成员函数不能用于初始化对象,因为这时只能叫赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;

class Test {
public:
const int x;
mutable int y;
Test() : x(111){}
int getY() {
return y;
}
const int getX() const { // 常成员函数, 注意标志是后面那个 const
y = 100; // 常成员函数不能改变对象的状态
//getY(); // 但我们可以用 mutable 关键字使在常成员函数中也可改变成员变量的值
return x;
}
};

int main() {
Test t;
cout << t.getX();
return 0;
}

关于const和引用

const 关键字修饰其实就是指定一个空间不可被修改, 其中的值就是固定的.
比如C++中的引用, 本质上是一个 指针常量, 固定指向的指针. 引用发生赋值实际上是取
常引用实际上是 (常量指针)常量, 固定指向且不可修改指向的值的指针.
但凡引用数据类型就必须涉及到’指针’, 指针实际上谈指向有些不准确的意味, 就是用来存放地址的特殊变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class P {
public:
int x;
P() {}
P(int x) : x(x) {}
};

int main() {
P p;
P& quote_p = p;
P* const ptr_p = &p; // 上面的引用相当于定义指针常量, 引用的写法是一种简化.
ptr_p->x = 1;

P& test_p = quote_p; // 引用赋值, test_p 也是指向p的指针常量, 指向确定了就不可修改.


P pp;
const P& quote_pp = pp; // 常引用, 不可修改指向的值
const P* const ptr_pp = &pp; // 上式相当于一个常量指针常量
return 0;
}

多文件结构和编译预处理命令

多文件组织结构

#include <filename>表示按照标准方式搜索要嵌入的文件,该文件位于编译环境的 include 子目录下,一般要嵌入系统提供的标准文件时采用这样的方式,
#include "filename", 表示首先在当前目录下搜索要嵌入的文件,如果没有,再按照标准方式搜索,对用户自己编写的文件一般采用这种方式,

决定一个声明放在源文件中还是头文件中的一般原则是,将需要分配空间的定义放在源文件中,例如函数的定义(需要为函数代码分配空间)、命名空间作用域中变量的定义(需要为变量分配空间)等;而将不需要分配空间的声明放在头文件中,例如类声明、外部函数的原型声明、外部变量的声明、基本数据类型常量的声明等。内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于那些需要被多个编译单元调用的内联函数,它们的代码应该被各个编译单元可见,这些内联函数的定义应当出现在头文件中。

外部变量与外部函数

如果一个变量除了在定义它的源文件中可以使用外,还能被其他文件使用,那么就称这个变量是外部变量。命名空间作用域中定义的变量,默认情况下都是外部变量,但在其他文件中如果需要使用这一变量,需要用extern关键字加以声明。

通常情况下,变量和函数的定义都放在源文件中,而对外部变量和外部函数的引用性声明则放在头文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// file one
#include <iostream>
using namespace std;
extern int i = 9; // 定义性声明, 初始化
extern void test() {
cout << &i << endl;
}
void add(){
i++;
}
// another
#include<iostream>
using namespace std;
extern int i;
extern void test(); // 引用性声明
int main() {
test();
cout << &i << endl;
return 0;
}

标准C++库

C语言中,系统函数、系统的外部变量和一些宏定义都放置在运行库( run-time library) 中。 c++ 的库中除继续保留了大部分 语言系统函数外,还加入了预定义的模板和类。标准 c++ 类库是一个极为灵活并可扩展的可重用软件模块的集合。标准 c++ 类与组件在逻辑上分为如下6种类型。包含了必要的头文件后,就可以使用其中预定义的内容了。

  • 输入输出类;
  • 容器类与 ADT( 抽象数据类型) ;
  • 存储管理类;
  • 算法;
  • 错误处理;
  • 运行环境支持。

编译预处理

在编译器对摞程序进行编译之前,首先要由预处理器对程序文本进行预处理。预处理器提供了一组编译预处理指令和预处理操作符。预处理指令实际上不是 c++ 语言的一部分,它只是用来扩充 c++ 程序设计的环境。所有的预处理指令在程序中都是以"#"来引导,每一条预处理指令单独占用一行,不要用分号结束。预处理指令可以根据需要出现在程序中的任何位置。

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#ifdef __STDC__
printf("yes\n");
#endif

#ifdef _DEBUG \
printf("debug begin!!\n"); // 这个 \ 是宏延续运算符, 与下一句之间必须只有一个换行
#endif //


#define squre(x) (x*x)
#define Max(x, y) (x > y ? x : y)
// 参数化宏来模拟函数

#define tokenpaster(n) printf ("token" #n " = %d\n", token##n)
// #标记粘连运算符, 允许宏定义中两个独立标记合并为一个, #n 代表取n的字符串, ##n表示将token与n两个字符串粘连成一个新标识符

int main() {

printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__); // 预定义宏
//printf("%s\n", __STDC__);

int token30 = 100;
tokenpaster(30);

printf("%d\n", squre(5));
printf("%d\n", Max(88, 99));
return 0;
}

typedef vs #define

typedef 仅限于为类型定义符号名称,#define不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

代码的编译连接与执行过程

编译
一个个源文件,经过编译系统的处理,生成目标文件的过程叫做编译。编译是对一个个源文件分别处理的,因此每个源文件构成了一个独立的编译单元,编译过程中不同的编译单元互不影响。
目标文件主要用来描述程序在运行过程中需要放在内存中的内容,这些内容包括两大类一一代码和数据。相应地,目标文件也分成代码段和数据段
代码段(. text)中的内容就是源文件中定义的一个个函数编译后得到的目标代码
数据段中包含对源文件中定义的各个静态生存期对象(包括基本类型变量)的描述。
数据段又分为初始化的数据段(. data) 和未初始化的数据段(. bss) 。

初始化的数据段中包括了那些在定义的同时设置了初值的静态生存期对象(通过执行构造函数的方式赋初值的不在此列)。这些对象在运行时占多少内存空间,在目标文件中就要提供多少空间存放它们的初值。
其他静态生存期对象,都放在未初始化的数据段中。由于它们没有静态的初值,目标文件中不需要保留专门空间存储它们的信息,只需记录这个段的大小。

不同编译单元间的相同变量或函数的联系要通过这些变量或函数的名字来建立,这些名字都存放在目标代码的符号表中。

符号表是用来把各个标识符名称和它们在各段中的地址关联起来的数据结构。具体地说,符号表应当包含若干个条目,每个静态生存期对象或函数都对应于符号表中的一个条目。这个条目存储了该静态生存期对象或函数的名字和它在该目标文件中的位置,位置是通过它所在那个段以及它相对于该段段首的偏移地址来表示。

对于那些在编译单元中被引用但未定义的外部变量、外部函数,在符号表中也有相关的条目,但条目中只有符号名,而位置信息是未定义的。

符号表中,函数并不只以它在源程序中的名字命名,函数在符号表中的名字至少要包括源程序中的函数名和参数表类型信息。

目标文件代码段的目标代码中对静态生存期对象的引用和对函数的调用所使用的地址都是未定义的,因为它们的地址在连接阶段才能确定。

在目标文件中还需要保存一些信息,用来将目标代码中的地址和符号表中的条目建立关联,这样到连接时,通过这些信息就可以将这些指令中的地址设置为有效的地址。这些信息称为重定位信息。

链接

在连接期间,需要将各个编译单元的目标文件和运行库当中被调用过的单元加以合并。运行库实际上就是一个个目标代码文件的集合,运行库的各个组成部分a.o这样的目标代码具有相同的结构。经过合并后,不同编译单元的代码段和两类数据段就分别合并到一起了,程序在运行时代码和静态数据需要占据的内存空间就全部已知了,因此所有代码和数据都可以被分配确定的地址了。

与此同时,各个目标文件的符号表也可以被综合起来,符号表的每个条目都会有确定的地址。重定位信息这时也能发挥作用了,各段代码中未定义的地址,都可以被替换为有效地址。

连接的对象除了用户源程序生成的目标文件外,还有系统的运行库。例如,执行输入输出功能,调用 sin.fabs 这类标准函数,都需要通过系统运行库。此外,系统运行库中还包括程序的引导代码。在执行 maln 函数之前,程序需要执行一些初始化工作;在 main函数返回后,需要通知操作系统程序执行完毕,这些都要由运行库中的代码来完成。

知识点:

  1. OOP基本特征: 抽象封装继承多态
  2. 通常情况下, using namespace 语句不宜放在头文件中,因为这会使一个命名空间不被察觉地对一个源文件开放。
  3. zu%, 格式化输出size_t 类型
  4. 出现不安全报错可以添加编译预处理指令#pragma warning(disable _code)
  5. mutable修饰的成员对象在任何时候都不会被视为常对象, 也就是说常对象的mutable对象成员是可变的
  6. 函数声明中可以不指定形参名, 只给类型.

参考

  1. 《C++语言程序设计(第4版)》 IBSN 9787302227984
  2. C++参考手册
  3. Microsoft C++文档
  • Author:

    slacr_

  • Copyright:

  • Published:

    April 26, 2023

  • Updated:

    April 26, 2023

Buy me a cup of coffee ☕.

1000000