C++数组
- 数组是元素的集合,按特定顺序排列的一堆东西
- C++数组就是表示一堆变量组成的集合,一般是一行相同类型的变量
- 数组就像在一个变量中有多个变量
例1
#include <iostream>
int main()
{
int example[5];
example[0] = 2;
int a = example[0];
std::cout << example[0] << std::endl;
std::cout << example << std::endl;
std::cin.get();
}
- example数组
- 已经为5个整型分配的空间
- 如果像要访问-1或5,会出现内存访问违规(Memory Access Violation )(在debug模式会报错,但在release模式不会)
- 注意:确保总是在数组的边界内写东西,不然的话,会导致很难调试的问题,因为修改了内存,而这些内存不是数组的一部分,会有可能是源代码中另一个变量的内存,这样的话,就会在不知不觉中修改了其他变量
- 确保设置了安全检查,确保写的东西没有超出界限
for 循环
例2
#include <iostream>
int main()
{
int example[5];
for (int i = 0; i < 5; i++)
{
example[i] = 2;
}
std::cout << example[0] << std::endl;
std::cout << example << std::endl;
std::cin.get();
}
-
索引从0到4,也可以写成<=4, 但一般不会这么做因为这涉及到性能问题,因为要多做一个等于的比较,所以,一般都写成<5
-
example是自己的内存地址,因为example是一个整型指针是连续的内存,每个整型占四个字节,总共5个整型,占20个字节
-
索引为2的话,则example地址偏移量为2*整型大小 = 2 * 4 = 8个字节
整型指针
例3
#include <iostream>
int main()
{
int example[5];
int* ptr = example;
for (int i = 0; i < 5; i++)
{
example[i] = 2;
}
example[2] = 5;
*(ptr + 2) = 6;
std::cin.get();
}
- 可以将整型数组赋值给整型指针
- 指针可以用 逆向引用(dereference) 来修改数组
- ptr + 2: 计算实际要加的字节数(偏移),但这个是取决于指针的类型,在这里,指针类型是整型,所以+2的效果相当于字节偏移量是4*2 = 8
- 也可以将指针转换成只有一个字节的指针类型,比如char*类型:
*(int *)((char* )ptr + 8) = 6
:
- 但是,如果将整型指针类型的ptr转换成char指针类型,+2就要变成+8
- 并且由于原类型是一个int*指针类型,所以加完偏移量之后需要著转回整型指针类型(int *)
- 最后,同样使用逆向引用修改指针指向的变量值
在堆(heap)上创建数组
- 通过new关键字创建一个对象(实例)
例4
#include <iostream>
int main()
{
int example[5];
for (int i = 0; i < 5; i++)
{
example[i] = 2;
}
int* another = new int[5];
for (int i = 0; i < 5; i++)
{
another[i] = 2;
}
delete[] another;
std::cin.get();
}
- 与之前的数组是一样的,只是生存期不同
- example是在栈上创建的,在作用域结束的时候,就会自动销毁
- another是在堆上创建的,直到程序销毁,都是出于活跃状态。需要用delete来删除
由于是数组,需要带[]来删除它
为什么动态地用new来分配,而不是在栈上创建呢
- 最大原因是生存期,用new分配的内存,直到手动删除都一直存在
- 如果有一个函数返回一个数组,就必须使用new关键字来分配,除非传入一个数组的地址参数
- 如果想返回一个数组,然而这个数组是在函数内创建的,就不会返回成功
间接寻址
- 我们有指针,指针会指向另一个内存块,这个内存块保存着我们的数组,这会产生内存 碎片(memory fragmentation) 和 缓丢失(cache miss)
例5
#include <iostream>
class Entity
{
public:
int example[5];
Entity()
{
for (int i = 0; i < 5; i++)
{
example[i] = 2;
}
}
};
int main()
{
Entity e;
std::cin.get();
}
- Entity e的内存地址&e存储着:02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
- 然而,如果用new关键字在堆上创建:
#include <iostream>
class Entity
{
public:
int* example = new int[5];
Entity()
{
for (int i = 0; i < 5; i++)
{
example[i] = 2;
}
}
};
int main()
{
Entity e;
std::cin.get();
}
- Entity e的内存地址&e存储着:58 76 78 00
- 内存地址00787658存储着:02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
- 这个就是所谓的内存间接寻址(memory indirection)
- 得到e的内存地址,它包含另一个内存地址,这才是存放数组的实际内存地址
- 所以,当我们想要访问这个数组的时候,代码是跳来跳去的建议在栈上创建数组来避免这种情况,因为内存的跳跃肯定会影响性能
C++11的数组
- std::array
- 内置的数据结构
- 有边界检查,记录数组大小
- 原始的数组是无法计算出数组的大小的:
- 没有example->size()
- 永远不要在数组内存中,访问数组的大小,因为这是危险的
int a[5]; int cout = sizeof(a) / sizeof(int);
可以给出数组的大小
sizeof(a) = 5*4 = 20
sizeof(int) = 4
cout = 20 / 4 = 5int* example = new int[5]; int cout = sizeof(example) / sizeof(int);
sizeof(example) = 4
sizeof(int) = 4
cout = 4/ 4=1- 但以上的方法不可靠,因为当把它放进某函数,或某些其他东西,它如果一旦变成指针,就完了
- 所以,通常的做法是先定义一个size常量,并把这个size常量赋给数组的大小
static const int exampleSize = 5; int* example[exampleSize];
在栈中申请内存时,其大小必须是一个编译时就知道的常量(静态常量)const int exampleSize = 5; int* example = new int[exampleSize];
或者在堆上申请内存,其大小不需要是一个静态常量
- std::array:
例6
#include <iostream>
#include <array>
class Entity
{
public:
static const int exampleSize = 5;
int* example = new int[exampleSize];
std::array<int, 5> another;
Entity()
{
for (int i = 0; i < another.size(); i++)
{
example[i] = 2;
}
}
};
int main()
{
Entity e;
std::cin.get();
}
- 会有一定的开销,因为会做边界检查
- 也保存了一个整数size
C++字符串
- 字符串是一个接一个字符的一组字符
- 字符串是能够表示和处理文本的方法
- 字符串实际上是字符数组
C风格字符串
例7
#include <iostream>
int main()
{
const char* name = "Cherno";
std::cin.get();
}
- 也可以char* name = “Cherno”, 但通常不这么做,因为我们不希望改变这个字符串
- 因为字符串是不可变的,不能扩展字符串并使它变大,因为这是一个固定分配的内存块
- char* 不意味着是在堆上分配的,不能调用delete来删除
字符串
- cherno后面接00, 表示结束符
- 内存是十六进制,按照ascii码进行编码,43的ascii码是C
例8
#include <iostream>
int main()
{
char* name = "Cherno";
std::cout << name << std::endl;
// name[2] = 'a';
std::cin.get();
}
- 假设name是一个数组指针,那么打印出来的是数组的地址;但是由于name是一个字符串指针,所以打印出来就是Cherno字符串
- 双引号默认是char*
- 字符数组:
char name2[6] = { 'C', 'h', 'e', 'r', 'n', 'o' };
单引号,在debug模型调试,会插入栈守卫之类的东西,这样就知道是不是在分配内存之外了
#include <iostream>
int main()
{
char* name = "Cherno";
char name2[7] = { 'C', 'h', 'e', 'r', 'n', 'o', '\0' };
int number[6] = { 0, 1, 2, 3, 4, 5 };
std::cout << name2 << std::endl;
std::cout << number << std::endl;
std::cin.get();
}
- 后面加一个\0
C++字符串String
- BaseString类,是一个模板类
- std::string, 是BaseString类的模板版本,模板参数是char,这个叫模板特化,template specialization
- 将baseString模板类中的模板参数设为char,意思是char是每个字符背后的实际类型
- std::string是什么?其实就是一组字符,以及背后一些可以操作的函数
- string有一个构造函数,接受char或const char参数
- string实际上是一个const char数组,而不是char数组
例9
#include <iostream>
#include <string>
int main()
{
std::string name = "Cherno";
std::cout << name << std::endl;
std::cin.get();
}
-
如果没有头文件#include 则输出流不允许字符串发送到cout流中,因为在string头文件内部,对操作符<<进行了字符串到输出流的重载
-
两个字符串相加:
std::string name = "Cherno" + "hello!";
报错,因为右边是两个字符串相加,但是大家都知道,字符串是不可变的,字符串是const char数组,不是真正的string, 所以右边是不能相加的 -
可以分开两行:
std::string name = "Cherno"; name += "Hello!";
这里的操作实际上是将一个指针,添加到了name,name是一个string,把它加到string上,而+=操作符已经被string重载了,所以可以这样写 -
或者显式添加std::string强制转换:
std::string name =std::string( "Cherno" )+ "Hello!";
创建了一个string(“Cherno”),然后添加这个字符数组“Hello!”给这个string -
寻找字符串的文本,用find():
std::string name =std::string( "Cherno" )+ "Hello!"; bool constains = name.find("no") != std::string::npos;
在“Cherno”寻找"no",判断是否等于std::string::npos(即不存在的位置);返回判断结果;name.find()会返回“no”所在的首位置
字符串传递给函数
例10
void PrintString(std::string string)
{
string += "h";
std::cout << string << std::endl;
}
- 不会这么写,因为std::string string 参数是一个拷贝
- 这对外面传进来的string对象没有任何改变,这里的修改仅限于函数作用内
为什么要复制整个字符串
- 意味着必须动态地在堆上分配一个全新的额char数组来存储已经得到的相同的文本
- 字符串复制实际上是很慢的
确保常量引用字符串
void PrintString(std::string& string)
{
string += "h";
std::cout << string << std::endl;
}
C++字符串字面量
- 字符串字面量是在双引号之间的一串字符
- 字符串最后有一个终止符
例11
#include <iostream>
#include <string>
#include <stdlib.h>
int main()
{
const char name[8] = "Che\0rno";
std::cout << strlen(name) << std::endl;
std::cin.get();
}
- char数组,name只有3个,就遇到了\0,结束
例12
#include <iostream>
#include <string>
#include <stdlib.h>
int main()
{
const char* name = "Cherno";
std::cout << strlen(name) << std::endl;
std::cin.get();
}
-
char指针数组
-
未定义是不被允许的,原因是,这里所做的是去了一个指向那个字符串字面量的内存位置的指针,而字符串字面量是存储在内存的只读部分
-
字符串存储在二进制文件的const部分,字符串常量被嵌入到二进制文件中。引用的时候,实际上引用到的是一个不能编辑的常量区域
-
char* name = "Cherno"; name[2] = 'a';
这样是不行的,这是一个未定义的行为,改不了,不能将e改成a;试图对只读内存进行写操作,如果确实想要修改它,出于某些原因是可以的; clang编译器:用字符数组则没有那个问题
宽字符wchar_t
const char* name = "Cherno";
const wchar_t* name2 = L"Cherno";
大写L是表示下面的字符串字面值由宽字符组成
const char* name = u8"Cherno" ;
const wchar_t* name2 = L"Cherno" ;
const char16_t* name3 = u"Cherno" ;
const char32_t* name4 = U"Cherno" ;
- wchar_t和char16_t之间的区别
using namespace std::string_literals;
std::string name0 = std::string("Cherno") + "hello";
关于字符串字面量的内存
- 字符串字面量永远保存在内存的只读区域内