文章目录
1.P31 数组
参考:视频 笔记
1.1.数组和指针
#include<iostream>
int main()
{
int example[5];
int* ptr = example; // 数组名就是一个指针
example[2] = 2;
*(ptr+2) = 3;
*(int*)((char*)ptr + 8) = 4;
std::cin.get();
}
数组存储的数据是连续的,在内存中每个整数是4个字节,所以我们得到的是一行20字节的内存。当我们通过example[i]来访问特定索引时,实际上是对内存取了一个偏移量。
注意数组名就是一个指针,它指向了这个数组内存的首地址。因为数组实际上只是一个指针,这里可以创建一个整型指针ptr,并赋值为example。
使用指针来访问数组的时候,若我们访问2号元素,则位置是从指针开始8个字节的偏移量。用指针简单地重写,指针算术上是ptr+2,然后解引用设置为想设置的数字。如果想使用字节偏移的地址,那么可以先把int指针转成char指针,然后偏移8位;最后赋值的之前为了不改变数组存储的数据类型,需要再转成int指针,最后解引用赋值。
1.2.手动计算数组长度的风险
#include<iostream>
int main()
{
// 对:在栈上分配的数组,sizeof得到数组的字节数
int example[5];
int size = sizeof(example) / sizeof(int);
// 错:在堆上分配的数组,赋值是一个指针,占内存就是4个字节
int* new_example = new int[5];
int new_size = sizeof(new_example) / sizeof(int);
std::cin.get();
}
如上述程序,如果是在栈上创建的数组,那么sizeof
得到的就是整个数组的字节大小,此时用sizeof
统计就是正确的。
如果是在堆上new的数组,由于赋值给变量是一个指针,所以sizeof
指针就是int大小,即4个字节。这样统计就是错误的。
因此必须手动维护数组的长度,即声明一个const变量。但是如果直接这样声明的话,在栈上分配数组的时候会报错,因为在栈上分配数组的时候大小必须是一个编译时就知道的常量。解决办法是把const变量声明为static,static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间。
class Entity
{
public:
// 错误方法
const int size = 5;
int example[size]; // 报错,因为在栈上为数组申请内存的时候,数组大小必须是一个编译时就知道的常量
// 正确方法:
static const int size = 5;
int example[size];
};
1.3.在堆上创建数组
1.3.1.堆栈数组的不同
在栈上创建,当跳出作用域范围时,它会被销毁。
为什么要动态地使用new来分配,而不是在栈上创建呢。最大的原因还是生存期的不同,用new分配的内存将一直存在直到删除它。如果是在堆上创建的话,直到程序把它销毁之前都是处于活动状态的,所以我们需要用delete关键字来删除。因为使用了数组的操作符[]来分配内存,我们还需要使用[]来删除它。
#include<iostream>
int main()
{
int example[5]; //在栈上创建
int* another = new int[5]; //在堆上创建
delete[] another;
std::cin.get();
}
1.3.2.堆上创建数组的间接寻址
#include<iostream>
class Entity
{
public:
// 栈
int example[5];
// 堆
int* example = new int[5];
Entity()
{
for (int i = 0;i < 5;i++)
example[i] = 2;
}
};
int main()
{
Entity e;
std::cin.get();
}
- 栈:类的地址就是成员变量中数组的首地址
- 堆:类的地址中存放的是一个指针,这个指针所指向的内存才是成员变量中数组存放的地方。所以应该在栈上创建数组来避免这种情况,因为这样在内存中跳跃肯定会影响性能。
1.4.C++11中的数组
在C++11中我们有标准数组std::array,这是一个内置数据结构在C++11库中。它有很多优点,例如边界检查,记录数组大小(原始数组无法计算大小,无法使用example.size())。
std::arry<int,5>another;
此时的another.size()大小为5。
2.P32 字符串
参考:视频 笔记
2.1.字符串本质
字符串实际上是字符数组,我们通常将字符串称为const char*。加上const是为了不改变这些值,因为无法扩展字符串使其变大。char*并不意味着它是在堆上分配的,不能通过调用delete来删除。
const char* name = "Cherno";
我们可以在内存视图的右边看到Cherno这个词,说明左边数字代表的是ASCII值。第7个字节被设为0,这被称为空终止字符,空终止字符是为了判断字符串的size。字符串从指针的内存地址开始,直到碰到0为止。
2.2.std::string
C++的标准库中有一个模板类叫BasicString,而string的类实际上就是BasicString类的模板参数是char。在c++中使用字符串应该使用std::string,它只是一个char数组和一些操作这些数组的函数。
string有一个构造函数,它接收char或const char参数,把鼠标悬停会发现实际上是一个const char数组而不是char数组。为什么通常把字符串赋值给const char* 而不是char*,因为本质上用双引号定义字符串时,在C++中是const char数组而不是char数组,这是通过char* 的隐式转换。
2.3.追加字符串
若想在Cherno后面加上hello,这里可能会出错。因为我们是想把两个const char类型的数组相加(显然不合法),双引号里的东西是const char数组而不是真正的字符串,不能把两个指针(数组就是指针)相加,不能将两个数组直接相加。
若想做这样的事,我们可以把它分成多行,这样做是将一个指针加到了name,name是一个字符串,把它加到字符串上,+=这个操作符在string类中被重载了。
2.4.字符串作为参数传递要使用引用
如果我们写了一个叫PrintString的函数想要传递一个字符串,不会简单的写std::string string然后打印string。
void PrintString(std::string string)
{
std::cout << string << std::endl;
}
这实际上是一个副本,如果这样把类(对象)传递给一个函数,实际上是在复制这个类(对象),这样会在堆上创建一个char数组然后复制string中的每个字符。在函数中对这个类(对象)进行的操作不会传递到原始的类(对象)中。
void PrintString(const std::string& string)
{
std::cout << string << std::endl;
}
可以通过常量引用来传递,在前面加上const和引用&。&告诉我们是一个引用,意味着它不会被复制,而const承诺我们不会修改它(string)。