[NKU]C++理论课 cours 3 数据抽象(封装->隐藏实现的手段,隐藏->封装的重要目标)
page 1 数据抽象 隐藏实现
Chapter 4: Data Abstraction
Improvements of C++
Size of an object
Inclusion guard
Nested Structure
Chapter 5: Hiding the implementation
Access control: public, private, friends
Declaring a nested structure as friend
Object layout
The keyword class
Application of the access control
Hiding the implementation
第4章:数据抽象
C++ 的改进
对象的大小 sizeof
运算符可用于获取对象的大小。
包含防护(头文件保护). 为了避免头文件的重复包含,C++ 使用包含防护机制(如 #ifndef
、#define
、#endif
)。
嵌套结构 在 C++ 中,可以在一个结构体或类中定义另一个结构体或类,这种结构称为嵌套结构。嵌套结构可以访问外部结构的成员。
第5章:隐藏实现
访问控制:public
、private
、friend
将嵌套结构声明为友元 可以将嵌套结构声明为友元,使其能够访问外部类的私有成员。
对象布局
class
关键字 class
是 C++ 中用于定义类的关键字。与 struct
不同,默认情况下,class
的成员是私有的,而 struct
的成员是公有的。
访问控制的应用 通过合理使用访问控制,可以隐藏类的实现细节,只暴露必要的接口。这有助于提高代码的封装性和可维护性。
隐藏实现 隐藏实现是数据抽象的核心思想之一。通过将类的实现细节隐藏在私有成员中,只通过公共接口与外部交互,可以减少依赖关系,提高代码的灵活性和安全性。
-
封装 是一种更广泛的概念,指的是将数据和行为组合在一起,并通过访问控制隐藏数据。
-
隐藏实现 是封装的一个重要目标,更侧重于隐藏类的内部实现细节,只暴露必要的接口。
在实际编程中,封装是实现隐藏实现的手段,而隐藏实现是封装的一个重要目标。
通过封装,可以实现隐藏实现,从而提高代码的安全性、可维护性和复用性。
page 2
main objectives
Object = characteristic + behavior
C++ advantages against C
How to organize header files
what should be put into a header file
how to avoid multiple declaration
how to use the header file
主要目标
对象=特征+行为
c++相对于C的优势
如何组织头文件
头文件里应该放些什么
如何避免多次申报
如何使用头文件
Main Objectives(主要目标)
数据抽象的主要目标:
-
隐藏复杂性:将复杂的实现细节隐藏起来,只暴露简单的接口。
-
提供简洁的接口:通过封装数据和行为,提供易于使用的接口。
-
增强代码复用性:通过封装和抽象,创建通用的类库,便于在不同项目中复用。
-
提高代码安全性:隐藏内部实现细节,防止外部代码直接访问或修改内部数据,避免数据被滥用。
-
便于维护和扩展:由于内部实现对外隐藏,修改内部实现不会影响到使用该类的代码,只要接口保持不变。
Object = Characteristic + Behavior(对象 = 特征 + 行为)
对象的定义:
-
对象(Object) 是面向对象编程中的基本单位,它封装了数据和操作这些数据的方法。
-
特征(Characteristic):对象的状态,通常用成员变量(属性)表示。
-
行为(Behavior):对象的行为,通常用成员函数(方法)表示。
page 3 动态数组
dynamic array 动态数组
storage 存储
size 大小
next 下一个
quantity 数量
functionality: add, fetch, inflate 功能:添加,获取,膨胀
1. 库的设计原则
-
封装性:将动态数组的实现细节隐藏起来,只暴露必要的接口(如
add
、fetch
)。 -
接口设计:提供简洁的接口,让用户可以方便地使用动态数组,而不需要了解内部实现。
-
模块化:将动态数组的功能划分为不同的模块(如初始化、添加元素add、获取元素fetch、扩展容量inflate等)。
2. 动态数组的实现思路
-
动态内存管理:使用
malloc
、realloc
和free
等函数动态分配和管理内存。 -
扩展性:当数组容量不足时,通过
inflate
函数扩展存储容量。 -
用户友好性:提供简单的接口(如
add
、fetch
),让用户可以方便地操作动态数组。
malloc
-
全称:Memory allocation(内存分配)
-
功能:分配一块指定大小的内存,并返回指向这块内存的指针。
-
原型:
c复制
void* malloc(size_t size);
-
说明:
-
size
是需要分配的内存大小(以字节为单位)。 -
返回值是一个
void*
类型的指针,指向分配的内存块的起始位置。 -
如果分配失败,返回
NULL
。 -
分配的内存内容是未初始化的,可能包含任意值。
-
2. realloc
-
全称:Resize allocation(重新分配内存)
-
功能:调整已分配内存块的大小。如果需要更大的内存块,可能会移动内存块的位置。
-
原型:
c复制
void* realloc(void* ptr, size_t new_size);
page4-9 动态数组
CLib.h
//CLib.h
typedef struct CStashTag {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
} CStash; // a place to hide something.
// } CStash;:在C语言中,CStash 是结构体的标签,为结构体定义了一个新的类型名。
void initialize (CStash* s, int size );
void cleanup (CStash* s );
int add (CStash* s, const void* element);
void* fetch (CStash* s, int index );
int count (CStash* s );
void inflate (CStash* s, int increase );
为什么 CStash
使用了 typedef
?
这主要是因为:
-
历史原因:
-
typedef struct
是C语言中的传统用法,C语言中必须使用typedef
来简化结构体的声明。 -
C++继承了C语言的语法,但C++提供了更灵活的语法,C++允许直接使用结构体名。
-
-
风格和习惯:
-
在C语言中,
typedef struct
是常见的写法。 -
在C++中,通常不使用
typedef
,而是直接使用结构体名。这种方式更简洁,也更符合C++的语法风格。
-
这里的语法结构可以分解为以下几个部分:
typedef
关键字:用于创建类型别名。struct CStashTag
:定义了一个结构体类型,其标签为CStashTag
。{ ... }
:结构体成员的定义。CStash
:通过typedef
,CStash
成为了struct CStashTag
的别名。
因此,在定义了这个结构体之后,你可以直接使用 CStash
来声明该类型的变量,而不需要再使用 struct CStashTag
。例如:
CLib.CPP
//CLib.CPP
#include "CLib.h"
#include <iostream>
#include <cassert>
using namespace std;
// Quantity of elements to add when increasing storage
const int increment = 100;
void initialize(CStash* s, int sz) {
s->size = sz; // 设置每个存储单元的大小
s->quantity = 0; // 初始化存储容量为 0(表示没有分配内存)
s->storage = 0; // 初始化存储指针为 NULL(表示没有分配内存)
s->next = 0; // 初始化下一个可用位置为 0
} // // 初始化intStash initialize(&intStash, sizeof(int));
int add(CStash* s, const void* element) {
if(s->next >= s->quantity) //Enough space left?
inflate(s, increment);
// Copy element into storage,
// starting at next empty space:
int startBytes = s->next * s->size;
unsigned char* e = (unsigned char*)element;
for(int i = 0; i < s->size; i++)
s->storage[startBytes + i] = e[i];
s->next++;
return(s->next - 1); // Index number
} // to be continued
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index);
if(index >= s->next)
return 0; // To indicate the end
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
int count(CStash* s) {
return s->next; // Elements in CStash
} // to be continued
void inflate(CStash* s, int increase) {
assert(increase > 0);
int newQuantity = s->quantity + increase;
int newBytes = newQuantity * s->size;
int oldBytes = s->quantity * s->size;
unsigned char* b = new unsigned char[newBytes];
for(int i = 0; i < oldBytes; i++)
b[i] = s->storage[ i ]; // Copy old to new
delete [ ](s->storage); // Old storage
s->storage = b; // Point to new memory
s->quantity = newQuantity;
} // to be continued
void cleanup(CStash* s) {
if(s->storage != 0) {
cout << "freeing storage" << endl;
delete [ ]s->storage;
}
}
-
如果你有一个结构体变量(而不是指针),你可以使用点操作符 (
.
) 来访问其成员。例如,如果stash
是一个CStash
类型的变量,那么你可以通过stash.quantity
来访问quantity
成员。 -
如果你有一个指向结构体的指针,你应该使用箭头操作符 (
->
) 来访问其成员。在你的例子中,s
是一个指向CStash
的指针,所以s->quantity
是正确的访问方式。
举例如下
(.)操作符访问结构体成员#include <stdio.h> typedef struct { int quantity; } CStash; int main() { CStash stash; // 创建一个结构体变量 stash.quantity = 5; // 使用点操作符访问并修改成员 printf("stash.quantity = %d\n", stash.quantity); // 输出成员的值 return 0; }
(->)操作符访问结构体成员
#include <stdio.h> typedef struct { int quantity; } CStash; int main() { CStash stashes[2]; // 创建一个结构体数组 stashes[0].quantity = 10; // 直接访问并修改数组第一个元素的成员 stashes[1].quantity = 20; // 直接访问并修改数组第二个元素的成员 // 创建一个指向结构体数组第一个元素的指针 CStash *s = &stashes[0]; // 使用箭头操作符访问并打印指针指向的结构体的成员 printf("s->quantity = %d\n", s->quantity); // 输出第一个元素的 quantity 值 // 移动指针到数组的第二个元素,并打印其成员 s = &stashes[1]; printf("s->quantity = %d\n", s->quantity); // 输出第二个元素的 quantity 值 return 0; }
page 10
Heap
new Type [ number_of_elements ]
return a pointer to the Type.
delete [ ] myArray
int *p = new int [100];
delete [ ] p;
memory leak, fragmented heap
堆
new Type [number_of_elements]
返回一个指向类型的指针。
删除[] myArray
Int *p = new Int [100];
删除[]p;
内存泄漏,碎片堆
1. 堆(Heap)
堆是程序运行时用于动态内存分配的内存区域。
与栈(Stack)不同,堆中的内存分配和释放是手动管理的,通常通过C++中的new
和delete
操作符来完成。
2. 动态数组的分配与释放
new Type[number_of_elements]
-
功能:在堆上分配一块连续的内存,用于存储一个动态数组。
-
返回值:返回一个指向数组首元素的指针。 int *p = new int [100];
-
示例:
int* p = new int[100]; // 分配一个包含100个int的动态数组
delete[] myArray
-
功能:释放通过
new[]
分配的动态数组。delete[] p; // 释放动态数组 -
注意:必须使用
delete[]
来释放new[]
分配的内存,而不是delete
。
否则,可能会导致未定义行为。 -
示例:
1 举例 new delete
int* p = new int; //
cout << "Value: " << *p << endl; // 输出未定义值,可能每次运行程序时都不一样。例如:Value: -12345678
或者:Value: 0
cout << "Value: " << *p << endl; // 输出 0
int* p = new int() //
int* p = new int(42); // new
操作符的返回值类型与分配的内存类型一致
delete p;
#include <iostream>
using namespace std;
int main() {
// 使用 new 分配单个 int
int* p = new int(42); // 初始化为 42
// 使用对象
cout << "Value: " << *p << endl;
// 使用 delete 释放单个对象
delete p;
return 0;
}
2 举例 new delete[]
int* arr = new int[5]; //数组中的值是未定义的,可能包含任意值。
arr[0]: -12345678 arr[1]: 42 arr[2]: 0 arr[3]: 314159 arr[4]: -1
int* arr = new int[5](); // 00000
int* arr = new int[5]{1,2,3,4,5};
delete[] arr;
#include <iostream>
using namespace std;
int main() {
// 使用 new[] 分配一个包含 5 个 int 的数组
int* arr = new int[5]{1, 2, 3, 4, 5}; // 初始化数组
// 使用数组
for (int i = 0; i < 5; i++) {
cout << "arr[" << i << "]: " << arr[i] << endl;
}
// 使用 delete[] 释放数组
delete[] arr;
return 0;
}
内存泄漏(Memory Leak)
内存泄漏是指程序分配了动态内存,但在使用完毕后没有正确释放,导致内存无法被其他程序或系统回收。
内存泄漏会导致程序占用的内存不断增加,最终可能导致程序崩溃或系统资源耗尽。
int* p = new int[100];
// 忘记释放内存
// delete[] p; // 如果忘记这一步,就会导致内存泄漏
堆碎片化(Fragmented Heap)
堆碎片化是指堆内存被频繁分配和释放后,导致堆空间变得碎片化。
碎片化的堆内存可能导致以下问题:
-
内存分配失败:即使堆中仍有足够的总内存,但由于碎片化,可能无法找到足够大的连续空间来满足新的分配请求。
-
性能下降:频繁的内存分配和释放会增加管理堆的开销。
void example() {
for (int i = 0; i < 1000; i++) {
int* p = new int[100];
delete[] p; // 正确释放内存
}
} //在这个例子中,虽然每次分配的内存都被正确释放,但频繁的分配和释放可能导致堆碎片化。
正确使用new[] delete[]避免 内存泄漏和堆碎片化
#include <iostream>
using namespace std;
int main() {
// 动态分配一个包含100个int的数组
int* p = new int[100];
// 使用数组
for (int i = 0; i < 100; i++) {
p[i] = i * i; // 初始化数组
}
// 输出数组内容
for (int i = 0; i < 100; i++) {
cout << "p[" << i << "]: " << p[i] << endl;
}
// 释放动态数组
delete[] p;
return 0;
}
page 11-13
CLabTest.cpp
#include "CLib.h" //CLib.h:假设这是一个自定义的头文件,定义了 CStash 结构体以及相关的函数(initialize、add、fetch、cleanup 等)。
#include <fstream> //文件
#include <iostream> //控制台
#include <string>//字符
#include <cassert> //assert
using namespace std;
int main( ) {
// Define variables at the beginning
// of the block, as in C:
CStash intStash, stringStash; //定义对象用于存储 int string
int i; //循环计算
char* cp; // 字符指针,提取字符串
ifstream in; //打开文件读取,输入流对象
string line; // 字符串变量,读取文件美航
const int bufsize = 80; 定义常量 整形 字符串最大长度80
// to be continued
// 初始化intStash
initialize(&intStash, sizeof(int));
for(i = 0; i < 100; i++)
add(&intStash, &i);
for(i = 0; i < count(&intStash); i++)
cout << "fetch(&intStash, " << i << ") = "
<< *(int*)fetch(&intStash, i)
<< endl;
// to be continued
// Holds 80-character strings:
initialize(&stringStash, sizeof(char)*bufsize);
in.open("CLibTest.cpp");
assert(in);
while(getline(in, line))
add(&stringStash, line.c_str());
i = 0;
while( (cp = (char*)fetch(&stringStash,i++) )!=0)
cout << "fetch(&stringStash, " << i << ") = "
<< cp << endl;
cleanup(&intStash);
cleanup(&stringStash);
}
完整代码
//CLib.h
typedef struct CStashTag {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
} CStash; // a place to hide something.
// } CStash;:在C语言中,CStash 是结构体的标签,为结构体定义了一个新的类型名。
void initialize (CStash* s, int size );
void cleanup (CStash* s );
int add (CStash* s, const void* element);
void* fetch (CStash* s, int index );
int count (CStash* s );
void inflate (CStash* s, int increase );
//CLib.CPP
#include "CLib.h"
#include <iostream>
#include <cassert>
using namespace std;
// Quantity of elements to add when increasing storage
const int increment = 100;
//初始化
void initialize(CStash* s, int sz) { // CStash* s表示需要一个指向 CStash 类型结构体的指针
s->size = sz; // 设置每个存储单元的大小每个存储单元的大小(以字节为单位)
s->quantity = 0; // 初始化存储容量为 0(表示没有分配内存) 表示当前分配的存储单元总数。
s->storage = 0; // 初始化存储指针为 NULL(表示没有分配内存)
s->next = 0; // 初始化下一个可用位置为 0, 当前已使用的存储单元数量, 调用 add 函数时,s->next 会递增,指向下一个空闲位置。
} // // 初始化intStash initialize(&intStash, sizeof(int));
//相加
int add(CStash* s, const void* element) {
if(s->next >= s->quantity) // Enough space left? 用于判断当前存储空间是否已满。如果已满,则需要扩展存储空间以容纳更多数据。
inflate(s, increment);
// Copy element into storage, 将element拷贝到storage
// starting at next empty space: 在下一个empty空间开始
int startBytes = s->next * s->size;
unsigned char* e = (unsigned char*)element;
for(int i = 0; i < s->size; i++)
s->storage[startBytes + i] = e[i];
s->next++;
return(s->next - 1); // Index number
} // add(&intStash, &i);
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index);
if(index >= s->next)
return 0; // To indicate the end
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
int count(CStash* s) {
return s->next; // Elements in CStash
} // to be continued
void inflate(CStash* s, int increase) {
assert(increase > 0);
int newQuantity = s->quantity + increase;
int newBytes = newQuantity * s->size;
int oldBytes = s->quantity * s->size;
unsigned char* b = new unsigned char[newBytes];
for(int i = 0; i < oldBytes; i++)
b[i] = s->storage[ i ]; // Copy old to new
delete [ ](s->storage); // Old storage
s->storage = b; // Point to new memory
s->quantity = newQuantity;
} // inflate(s, increment);
void cleanup(CStash* s) {
if(s->storage != 0) {
cout << "freeing storage" << endl;
delete [ ]s->storage;
}
}
#include "CLib.h" //CLib.h:假设这是一个自定义的头文件,定义了 CStash 结构体以及相关的函数(initialize、add、fetch、cleanup 等)。
#include <fstream> //文件
#include <iostream> //控制台
#include <string>//字符
#include <cassert> //assert
using namespace std;
int main( ) {
// Define variables at the beginning
// of the block, as in C:
CStash intStash, stringStash; //定义对象用于存储 int string
int i; //循环计算
char* cp; // 字符指针char point,提取字符串
ifstream in; //打开文件读取,输入流对象
string line; // 字符串变量,读取文件美航
const int bufsize = 80; 定义常量 整形 字符串最大长度80
// to be continued
// 初始化intStash
initialize(&intStash, sizeof(int)); // &intStash 获取intStash变量的地址
for(i = 0; i < 100; i++)
add(&intStash, &i); // &intStash 获取了 intStash 的地址,并将其传递给 initialize 函数。 initialize 函数的参数是 CStash* s,即一个指向 CStash 类型的指针。因此,&intStash 是一个指向 CStash 的指针,而不是引用。
for(i = 0; i < count(&intStash); i++)
cout << "fetch(&intStash, " << i << ") = "
<< *(int*)fetch(&intStash, i)
<< endl;
// to be continued
// Holds 80-character strings:
initialize(&stringStash, sizeof(char)*bufsize);
in.open("CLibTest.cpp");
assert(in);
while(getline(in, line))
add(&stringStash, line.c_str());
i = 0;
while( (cp = (char*)fetch(&stringStash,i++) )!=0)
cout << "fetch(&stringStash, " << i << ") = "
<< cp << endl;
cleanup(&intStash);
cleanup(&stringStash);
}
代码功能概述
这段代码实现了一个简单的动态内存管理工具 CStash
,用于存储不同类型的数据(如 int
和 string
)。它提供了以下功能:
-
初始化:通过
initialize
函数设置存储单元的大小和初始状态。 -
添加数据:通过
add
函数将数据逐字节存储到动态分配的内存中。 -
获取数据:通过
fetch
函数根据索引获取存储的数据。 -
扩展内存:通过
inflate
函数动态扩展存储空间。 -
清理内存:通过
cleanup
函数释放动态分配的内存。
main()
函数的功能
main()
函数演示了如何使用 CStash
存储两种类型的数据:
-
存储
int
类型数据:-
初始化一个
CStash
对象intStash
,每个存储单元的大小为sizeof(int)
。 -
向
intStash
中添加 100 个整数。 -
遍历并打印所有存储的整数。
-
-
存储
string
类型数据:-
初始化另一个
CStash
对象stringStash
,每个存储单元的大小为sizeof(char) * bufsize
(80 字节)。 -
从文件
CLibTest.cpp
中逐行读取字符串,并将其存储到stringStash
中。 -
遍历并打印所有存储的字符串。
-
最后,使用 cleanup
函数释放动态分配的内存。
输出内容
假设文件 CLibTest.cpp
中有以下内容:
复制
Line 1
Line 2
Line 3
程序的输出将是:
复制
fetch(&intStash, 0) = 0
fetch(&intStash, 1) = 1
fetch(&intStash, 2) = 2
...
fetch(&intStash, 99) = 99
fetch(&stringStash, 0) = Line 1
fetch(&stringStash, 1) = Line 2
fetch(&stringStash, 2) = Line 3
总结
这段代码实现了一个通用的动态内存管理工具 CStash
,用于存储不同类型的数据。main()
函数演示了如何使用 CStash
存储 int
和 string
类型的数据,并从文件中读取字符串。程序的输出是存储的整数和字符串内容。
逐元素说明
unsigned char*
unsigned char*
是一个指向 unsigned char
类型的指针。
为了更好地理解它的含义和用途,我们需要先了解 unsigned char
和指针的基本概念。
1. unsigned char
-
unsigned char
是一种基本数据类型,表示一个无符号字符。 -
它通常占用 1字节(8位)的内存空间。
-
由于它是无符号的,其取值范围是 0 到 255(即
0x00
到0xFF
)。 -
unsigned char
常用于表示单个字节的数据,尤其是在需要处理二进制数据或内存操作时。
2. 指针
-
指针 是一个变量,用于存储另一个变量的内存地址。
-
指针的类型决定了它指向的数据类型。例如:
-
int*
指向int
类型的数据。 -
char*
指向char
类型的数据。 -
unsigned char*
指向unsigned char
类型的数据。
-
3. unsigned char*
-
unsigned char*
是一个指向unsigned char
的指针,用于逐字节访问内存。 -
它可以用来逐字节操作数据,而不需要关心数据的具体类型。这使得它非常适合处理二进制数据、内存拷贝或低级内存操作。
逐字节访问数据:
unsigned char data[] = {0x12, 0x34, 0x56, 0x78};
unsigned char* ptr = data;
for (int i = 0; i < 4; i++) {
cout << hex << (int)ptr[i] << " "; // 输出:12 34 56 78
}
内存拷贝:
int value = 0x12345678; // 定义一个32(4x8)位整数
unsigned char* src = (unsigned char*)&value; // 将 value 的地址转换为 unsigned char* 指针
unsigned char* dst = new unsigned char[4]; // 分配4字节的动态内存,一个字节8位,4x8=32
for (int i = 0; i < 4; i++) {
dst[i] = src[i]; // 逐字节复制
}
delete[] dst; // 释放动态分配的内存
逐字节访问
-
unsigned char* src = (unsigned char*)&value;
:将value
的地址转换为unsigned char*
指针,从而可以逐字节访问value
的内存。 -
unsigned char* dst = new unsigned char[4];
:分配4字节的动态内存,用于存储复制的数据。 -
dst[i] = src[i];
:逐字节将value
的内容复制到dst
中。
16位一个字节,8位半个字节
1. 十六进制数 0x12345678
的含义
0x12345678
是一个十六进制数,表示的是一个 32(4x8)位的整数。
它由8个十六进制数字组成,每个十六进制数字占用 4位(半字节),因此:
-
8个十六进制数字 = 32位 = 4字节。
具体来说:
-
0x12
表示十六进制的12
。 -
0x34
表示十六进制的34
。 -
0x56
表示十六进制的56
。 -
0x78
表示十六进制的78
。
2. 十六进制数的位数
十六进制数的位数取决于它表示的数值大小。例如:
-
0x12
是一个 2位 的十六进制数,表示一个字节(8位)。 -
0x1234
是一个 4位 的十六进制数,表示两个字节(16位)。 -
0x12345678
是一个 8位 的十六进制数,表示四个字节(32位)。
因此,0x12345678
是一个 32位的整数,需要 4个字节 来存储。
3. 内存布局
在你的代码中,int value = 0x12345678;
定义了一个32位的整数变量 value
,它的值是 0x12345678
。
这个值在内存中以字节的形式存储,具体布局取决于系统的字节序(大端或小端)。
疑问
unsigned char 取值范围是 0 到 255(16x16)(即
0x00
到0xFF
)?
1字节8位8个二进制位,0000 0000
0x12
表示二进制的0001 0010
。十进制的 18。1×16^1+2×16^0=16+2=18
0xFF
表示二进制的1111 1111
。
0xFF
是十六进制表示法中的一种写法
0x00
表示二进制的0000 0000
,十进制的 0。
0xFF
表示二进制的1111 1111
,十进制的 255。
1. 十六进制表示法(Hexadecimal)
十六进制是一种基于16的数制,用于表示数字。它使用16个符号:0-9
和 A-F
(10-15),其中:
-
0-9
表示数字 0 到 9。 -
A
表示 10,B
表示 11,C
表示 12,D
表示 13,E
表示 14,F
表示 15。
十六进制的表示方式
在C和C++中,十六进制数通常以 0x
或 0X
开头。例如:
-
0x12
表示十六进制数12
。 -
0xFF
表示十六进制数FF
。
为什么用十六进制?
十六进制在计算机编程中非常常用,因为它可以简洁地表示二进制数据。
每1个十六进制数字对应4个二进制位(bit),因此:
-
1字节(8位,8个二进制位(bit))可以用2个十六进制数字表示。
例如:
-
0x12
表示二进制的0001 0010
。 -
0xFF
表示二进制的1111 1111
。
2. unsigned char
的取值范围
unsigned char
是一种无符号字符类型,通常占用 1字节(8位)的内存。
由于它是无符号的,其取值范围是 0 到 255(16x16)。
为什么是 0 到 255?
1字节有8位,每一位可以是 0
或 1
。因此,1字节可以表示的数值范围是:
-
最小值:
0000 0000
(二进制)= 0(十进制) -
最大值:
1111 1111
(二进制)= 255(十进制)
用十六进制表示:
-
0x00
表示二进制的0000 0000
,十进制的 0。 -
0xFF
表示二进制的1111 1111
,十进制的 255。
3. 为什么有 0xFF
?
0xFF
是十六进制表示法中的一种写法,表示一个字节的所有位都被设置为 1
。它在编程中非常常用,用于表示:
-
全1掩码:在位操作中,
0xFF
可以用来将一个字节的所有位都设置为1
。 -
最大值:在无符号字符类型中,
0xFF
表示最大值 255。
4. 总结
-
0x
:表示十六进制数的前缀。 -
FF
:表示十六进制的最大值,对应二进制的1111 1111
,十进制的 255。 -
unsigned char
:无符号字符类型,取值范围是 0 到 255,可以用十六进制表示为0x00
到0xFF
。
add说明
int add(CStash* s, const void* element){...}
int add(CStash* s, const void* element) {
if (s->next >= s->quantity) { // 检查是否有足够的空间
inflate(s, increment); // 如果空间不足,扩展存储空间
}
int startBytes = s->next * s->size; // 计算下一个存储位置的起始字节
unsigned char* e = (unsigned char*)element;
// 将元素逐字节复制到存储空间
for (int i = 0; i < s->size; i++) {
s->storage[startBytes + i] = e[i];
}
s->next++; // 更新下一个可用位置(空闲存储单元的索引)
return (s->next - 1); // 返回当前元素的索引(表示刚刚存储的元素的索引)
}
int startBytes = s->next * s->size;
-
用于确定新元素在动态分配的内存中的存储位置
-
s->next
:表示当前已使用的存储单元数量,同时也是下一个空闲存储单元的索引。 -
s->size
:表示每个存储单元的大小(以字节为单位)。 -
startBytes
:计算从动态分配的内存的起始位置到下一个空闲存储单元的起始字节偏移量。
具体逻辑
假设:
-
s->next = 3
,表示已经有3个元素被存储,下一个元素将存储在第4个位置。 -
s->size = 4
,表示每个存储单元的大小为4字节(例如,存储int
类型的数据)。
那么:
int startBytes = s->next * s->size; // 3 * 4 = 12
这意味着下一个元素的存储位置从动态分配的内存的第12字节开始。
详细解释
-
计算起始字节偏移量:
-
startBytes = s->next * s->size
计算从动态分配的内存的起始位置到下一个空闲存储单元的起始字节偏移量。 -
例如,如果
s->next = 3
,s->size = 4
,则startBytes = 12
,表示下一个元素的存储位置从第12字节开始。
-
-
逐字节复制数据:
-
使用
unsigned char*
指针逐字节将element
的内容复制到动态分配的内存中。 -
s->storage[startBytes + i] = e[i];
将element
的第i
个字节复制到s->storage
的startBytes + i
位置。
-
-
更新
s->next
:-
s->next++
表示下一个可用位置的索引加1,为下一个元素的存储做好准备。
-
总结
-
int startBytes = s->next * s->size;
:-
计算从动态分配的内存的起始位置到下一个空闲存储单元的起始字节偏移量。
-
用于确定新元素在动态分配的内存中的存储位置。
-
假设当前存储的元素是int类型的1, int是4个字节(一个字节是8位)
1. 十进制 1
二进制 0000 0000 0000 0000 0000 0000 0000 0001
十六进制 0x00 00 00 01
在小端序中,内存布局为:
-
地址 0:
0x01
(最低字节) -
地址 1:
0x00
-
地址 2:
0x00
-
地址 3:
0x00
(最高字节)
2. element
和 e
的关系
-
element
是一个指向整数1
的指针。 -
e
是将element
转换为unsigned char*
类型后的指针,用于逐字节访问数据。
3. e[i]
的内容
假设 element
指向整数 1
,那么 e
逐字节访问的内容如下:
-
e[0]
:0x01
(最低字节) -
e[1]
:0x00
-
e[2]
:0x00
-
e[3]
:0x00
(最高字节)
内存复制过程
假设 s->size = 4
,s->next = 0
,startBytes = 0
:
-
s->storage[0] = e[0]
:0x01
-
s->storage[1] = e[1]
:0x00
-
s->storage[2] = e[2]
:0x00
-
s->storage[3] = e[3]
:0x00
总结
-
如果当前存储的元素是整数
1
,e[i]
的内容如下:-
e[0]
:0x01
-
e[1]
:0x00
-
e[2]
:0x00
-
e[3]
:0x00
-
-
这些值将被逐字节复制到动态分配的内存中。
-
这种逐字节操作使得
CStash
可以存储不同类型的数据,而不需要关心数据的具体类型。
值得注意的是这里返回之后并没有打印
for(i = 0; i < 100; i++){
add(&intStash, &i);
}
// 可以修改为
for(i = 0; i < 100; i++){
int index = add(&intStash, &i);
cout<< "当前索引:" << index <<endl;
}//这将输出每次添加元素的索引,帮助你验证 add 函数是否正确工作。
修改后的输出结果
当前索引:0
当前索引:1
当前索引:2
...
当前索引:99
fetch(&intStash, 0) = 0
fetch(&intStash, 1) = 1
fetch(&intStash, 2) = 2
...
fetch(&intStash, 99) = 99
fetch(&stringStash, 1) = Line 1
fetch(&stringStash, 2) = Line 2
fetch(&stringStash, 3) = Line 3
inflate说明
void inflate(CStash* s, int increase) {...}
inflate
函数的作用是动态扩展 CStash
的存储空间。
当当前存储空间不足时,这个函数会被调用,以分配更多的内存,并将旧数据复制到新的内存中。让我们逐步解析这个函数的逻辑。
void inflate(CStash* s, int increase) {
assert(increase > 0); // 确保增加的数量大于 0 // const int increment = 100;
int newQuantity = s->quantity + increase; // 计算新的存储单元数量
int newBytes = newQuantity * s->size; // 计算新的总字节数
int oldBytes = s->quantity * s->size; // 计算旧的总字节数
unsigned char* b = new unsigned char[newBytes]; // 分配新的内存
// 将旧数据从旧内存复制到新内存
for (int i = 0; i < oldBytes; i++) {
b[i] = s->storage[i];
}
// 释放旧内存
delete[] s->storage;
// 更新存储指针和存储单元数量
s->storage = b;
s->quantity = newQuantity;
}
详细解释
1. 参数
-
CStash* s
:指向CStash
结构体的指针,表示需要扩展存储空间的对象s。 -
int increase
:表示需要增加的存储单元数量。
2. 断言
assert(increase > 0);
-
作用:确保
increase
大于 0,避免无效的内存扩展操作。 -
如果
increase <= 0
,程序会触发断言失败并终止。
3. 计算新的存储单元数量(旧的quantity+新的increase)
int newQuantity = s->quantity + increase;
-
s->quantity
:当前已分配的存储单元数量。 -
increase
:需要增加的存储单元数量。 -
newQuantity
:扩展后的总存储单元数量。
4. 计算新的总字节数((旧的quantity+新的increase)*4 )
int newBytes = newQuantity * s->size;
-
s->size
:每个存储单元的大小(以字节为单位)
int是4个字节,->size
被设置为sizeof(int)是4
。 -
newBytes
:扩展后的总字节数。
5. 计算旧的总字节数( 旧的quantity*4 )
int oldBytes = s->quantity * s->size;
-
oldBytes
:当前已分配的总字节数。
6. 分配新的内存
unsigned char* b = new unsigned char[newBytes];
-
分配新的内存,大小为
newBytes
字节,并返回一个指向这块内存的指针b
-
b
是指向新分配内存的指针。 -
unsigned char
:无符号字符类型,通常占用 1字节 的内存空间。 -
newBytes
:表示需要分配的总字节数。 -
new unsigned char[newBytes]
:使用new
运算符动态分配一块内存,大小为newBytes
字节。 -
unsigned char* b
:b
是一个指向unsigned char
的指针,用于存储分配的内存的地址。
7. 复制旧数据到新内存
for (int i = 0; i < oldBytes; i++) {
b[i] = s->storage[i];
}
-
使用逐字节复制的方式,将旧内存中的数据复制到新内存中。
-
这确保了旧数据不会丢失。
8. 释放旧内存
delete[] s->storage;
-
释放旧的内存,避免内存泄漏。
-
s->storage
是指向旧内存的指针。
9. 更新存储指针和存储单元数量
s->storage = b;
s->quantity = newQuantity;
-
更新
CStash
的存储指针,指向新的内存。 -
更新存储单元数量为新的数量。
完整逻辑
-
计算新的存储单元数量(newQuantity = s->quantity+increase)和新的总字节数(newBytes = (s->quantity+increase)*4)。
-
分配新的内存 (unsigned char* b = new unsigned char[newBytes])。
-
将旧数据从旧内存复制到新内存。
unsigned char* b = new unsigned char[newBytes];
for(int i = 0; i < oldBytes; i++) {
b[i] = s->storage[ i ]; } -
释放旧内存。
delete[] (s->storage); -
更新存储指针(s->storage)和存储单元数量(s->quantity)。
s->storage = b; // Point to new memory
s->quantity = newQuantity;
打印展示
声明int count(CStash* s){...}
调用count(&intStash)
调用fetch(&Stash)
int count(CStash* s) {
return s->next; // Elements in CStash
} // to be continued
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index);
if(index >= s->next)
return 0; // To indicate the end
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
for(i = 0; i < count(&intStash); i++)
cout << "fetch(&intStash, " << i << ") = "
<< *(int*)fetch(&intStash, i)
<< endl;
输出结果
fetch(&intStash, 0) = 0
fetch(&intStash, 1) = 1
fetch(&intStash, 2) = 2
...
fetch(&intStash, 99) = 99
这段代码的作用是遍历 intStash
中存储的内容(所有整数),并通过 fetch
函数逐个获取这些整数的值,然后打印出来。
fetch
void* fetch(CStash* s, int index) {...} 返回指针
fetch
函数的作用是根据给定的索引 index
,从 CStash
的动态存储空间中获取指定元素的地址。让我们逐步解析这个函数的逻辑。
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index); // 确保索引非负
if (index >= s->next) // 检查索引 index 是否超出了当前已存储的元素范围。如果索引超出范围,返回 NULL
return 0;
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
if (index >= s->next) 一定不会错,
打印里面的 调用*(int*)fetch(&intStash, i),
调用的声明里是void* fetch(CStash* s, int index)
i就是 index , i(index)本身< count(&intStash),
int count(CStash* s) {
return s->next;
}
count(&intStash) 也就是s->next,
因此一定是index < s->next
核心目的是:
检查索引 index 是否超出了当前已存储的元素范围 s->next
1. void* fetch(CStash* s, int index)
的返回类型
fetch
函数的返回类型是 void*
,表示它返回一个指向 void
的指针。
在这种情况下,return 0;
和 return NULL;
是等价的。
-
0
:在C和C++中,0
可以隐式转换为任何指针类型,表示空指针(NULL
)。 -
NULL
:是一个宏定义,通常在C中定义为((void*)0)
,在C++中定义为nullptr
(C++11及以后)。
因此,return 0;
和 return NULL;
在这里的效果是相同的,都表示返回一个空指针。
详细解释 &(s->storage[index * s->size])
//调用*(int*)fetch(&intStash, i)
//fetch定义
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index); // 确保索引非负
if (index >= s->next) // 检查索引 index 是否超出了当前已存储的元素范围。如果索引超出范围,返回 NULL
return 0;
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
1. s->storage
-
s->storage
:这是CStash
结构体中的一个成员变量,类型为unsigned char*
(无符号字符类型的指针),指向动态分配的内存。
这块内存用于存储所有元素,每个元素占用s->size
字节。
2. index * s->size
-
index
:要访问的元素的索引。 -
s->size
:每个存储单元的大小(以字节为单位)。 -
index * s->size
:计算目标元素的起始字节偏移量。例如:-
如果
index = 2
,s->size = 4
,则index * s->size = 8
,
表示目标元素从第 8 字节开始。
-
3. s->storage[index * s->size]
-
s->storage[index * s->size]
:访问动态分配的内存中,
从起始位置偏移index * s->size
字节的位置。
这表示目标元素的第一个字节。
4. &(s->storage[index * s->size])
-
&
:取地址操作符,用于获取变量或内存位置的地址。 -
&(s->storage[index * s->size])
:获取目标元素的地址,并返回一个指向该地址的指针。
完整逻辑
这行代码的作用是:
-
计算目标元素的起始字节偏移量:
index * s->size
。 -
访问动态分配的内存中,从起始位置偏移
index * s->size
字节的位置。 -
获取该位置的地址,并返回一个指向该地址的指针。
示例
假设:
-
s->storage
指向一块动态分配的内存,大小为 400 字节
(100 个存储单元,每个单元 4 字节)。 -
s->size = 4
,表示每个存储单元的大小为 4 字节。 -
调用
fetch(s, 2)
,即获取索引为 2 的元素。
执行过程:
-
计算偏移量:
index * s->size = 2 * 4 = 8
-
访问内存位置:
s->storage[8] // 访问第 8 字节
-
获取地址:
&(s->storage[8]) // 获取第 8 字节的地址
返回值是一个指向目标元素的指针,类型为 void*
。
返回值
-
返回值类型:
void*
,表示返回的是一个通用指针,指向目标元素的地址。 -
调用方处理:调用方需要将返回的
void*
指针转换为具体的类型指针(如int*
或char*
),然后解引用以获取实际的值。例如:int* fetchedValue = (int*)fetch(&intStash, 2); cout << "Value at index 2: " << *fetchedValue << endl;
总结
-
&(s->storage[index * s->size])
:计算并返回指定索引index
处的元素的地址。 -
作用:通过计算偏移量,访问动态分配的内存中的目标元素,并返回其地址。
-
返回值:一个指向目标元素的
void*
类型指针,调用方需要根据存储的数据类型进行适当的转换和解引用。
*(int*)fetch(&intStash, i)解释
int count(CStash* s) {
return s->next; // Elements in CStash
}
//调用*(int*)fetch(&intStash, i)
//fetch定义
void* fetch(CStash* s, int index) {
// Check index boundaries:
assert(0 <= index); // 确保索引非负
if (index >= s->next) // 检查索引 index 是否超出了当前已存储的元素范围。如果索引超出范围,返回 NULL
return 0;
// Produce pointer to desired element:
return &(s->storage[index * s->size]);
}
for(i = 0; i < count(&intStash); i++)
cout << "fetch(&intStash, " << i << ") = "
<< *(int*)fetch(&intStash, i)
<< endl;
fetch函数: fetch
函数接收一个指向CStash
结构体的指针和一个索引index
,它返回指向存储在CStash
中该索引位置元素的指针。
由于storage
是一个unsigned char*
类型的指针,初始化时是=0 =NULL,
fetch
返回的也是void*
类型的指针,即一个通用指针,指向任何类型的数据。
类型转换和解引用: 当我们调用fetch(&intStash, i)
时,它返回指向intStash
中索引为i
的元素的void*
类型的指针。
由于我们知道intStash
是用来存储int
类型元素的(因为我们之前用sizeof(int)
初始化了它),我们需要将这个void*
类型的指针转换为int*
类型的指针,以便能够正确地解引用它以获取存储的整数值。
-
*(int*)fetch(&intStash, i)
解释:fetch(&intStash, i)
: 调用fetch
函数,获取指向intStash
中索引为i
的元素的void*
指针。(int*)fetch(&intStash, i)
: 将这个void*
指针转换为int*
指针。*(int*)fetch(&intStash, i)
: 解引用这个int*
指针,获取存储的整数值。
cleanup(&intStash);
void cleanup(CStash* s) {
if(s->storage != 0) {
cout << "freeing storage" << endl;
delete [ ]s->storage;
}
}
字符型结构体stringStash 解释
代码 仅包含stringStash部分结构体
//CLib.h
typedef struct CStashTag {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
} CStash; // a place to hide something.
#include <cassert> //assert
using namespace std;
int main( ) {
// Define variables at the beginning
// of the block, as in C:
CStash intStash, stringStash; //定义对象用于存储 int string
int i; //循环计算
char* cp; // 字符指针char point,提取字符串
ifstream in; //打开文件读取,输入流对象
string line; // 字符串变量,读取文件美航
const int bufsize = 80; 定义常量 整形 字符串最大长度80
// Holds 80-character strings:
initialize(&stringStash, sizeof(char)*bufsize);
in.open("CLibTest.cpp");
assert(in);
while(getline(in, line))
add(&stringStash, line.c_str());
i = 0;
while( (cp = (char*)fetch(&stringStash,i++) )!=0)
cout << "fetch(&stringStash, " << i << ") = "
<< cp << endl;
cleanup(&intStash);
cleanup(&stringStash);
initialize(&stringStash, sizeof(char)*bufsize);
- 第一个参数是
&stringStash
,这是stringStash
的地址。 stringStash
可能是一个结构体。使用了地址运算符&
。- 第二个参数是
sizeof(char)*bufsize
。这里使用了sizeof
运算符来计算char
类型的大小(在大多数平台上,sizeof(char)
的值都是 1 字节),然后将其乘以bufsize
。这个表达式的目的是计算一个能够存储80个字符的数组所需的字节数。
由于sizeof(char)
通常为 1,这个表达式简化为1 * 80
,即 80 字节。
这个值作为参数传递给initialize
函数,可能用于为stringStash
分配足够的内存空间来存储80个字符的字符串。
include<fstream>
ifstream in;
in.open("CLibTest.cpp");
assert(in);
assert(in);
来检查文件是否打开是不正确的,因为 std::ifstream
对象在转换为布尔值时,只有在文件成功打开时才会转换为 true
while(getline(in, line))
add(&stringStash, line.c_str());
line.c_str()
的作用
-
line
是一个std::string
对象。 -
c_str()
是std::string
类的成员函数,返回一个指向内部字符数组的const char*
指针。 -
返回的指针指向的字符数组以
\0
结尾,符合 C 风格字符串的格式。 -
.c_str()
在 C++ 的std::string
类中返回的是一个指向字符串内部字符数组首个字符的指针。
也就是说,它指向的是字符数组的第一个字符的地址,而不是末尾的空字符\0
。 -
.c_str()
成员函数时,返回的 C 风格字符串(即以\0
结尾的字符数组)是由std::string
对象内部自动管理的,包括在字符串的末尾添加空字符\0
。
这个空字符\0
是 C 风格字符串的标准表示方式,用于标识字符串的结束。
std::string cppString = "Hello, World!";
const char* cString = cppString.c_str();
// 输出 C 风格字符串
std::cout << cString << std::endl;