💻 CSC3200 LEC3-5 C++进阶
CSC3200 LEC 3-4 数组、指针、流
数组和指针
数组和指针是两个完全不同的数据类型,但是两者有紧密关系,且语法有相互兼容性。
C 风格的字符数组
- 保留方法:
a.c_str()会将字符串 a 转为 const char*。其作用相当于&a[0]。 - 字符串长度用 string 成员方法
.size()或.length(),作为数组的指针长度用strlen()。 - 作为指针,无论是否为 const,都可以进行偏移操作:
s.c_str()[n] ≡ s.c_str()+n;都可以进行偏移量计算:s.c_str()[5]-s.c_str[0] == 5 - 这是指针与数组语法的相互兼容的经典例子。
string s = "hello";
s.length(); // 5
cout << boolalpha << (strlen(s.c_str()) == (s.c_str[4]-s.c_str[0])) << '\n'; // True
cout << toupper(s.c_str()[0]); // 'H'(toupper 只能处理字符)
- **注意!**对于 string x,
&x的类型为string*,而x.c_str()的结果为const char*,两者无法直接相互赋值!以下是与x.c_str()类型相同的操作:
char* arr = new char[100]; // 堆上分配,手动释放: delete[] arr;
char arr[100]; // 栈上分配,自动释放
strlen 的定义
int strlen1(char* s){ int n=0; while(s[n] != '\0') n++; return n; }
int strlen2(char* s){ int n=0; while(*s++ != '\0') n++; return n; }
int strlen3(char* s){ char* p=s; while(*p!='\0') ++p; return p - s; }
1、n 作为字符指针数组的索引和计数器,自增直到\0。
2、n 只作为计数器,*(s++) 让指针自增直到指向\0。
3、不用计数器,直接计算首尾字符的指针偏移量。
- 指针自增
*p++的含义等于*(p++),是 “返回指针指向值,然后自增指针”:
string a = "hello";
const char* p = a.c_str(); // 或者 char* p = &a[0];
while (*p) {
cout << *p << '\n';
p = p + 1;
}
- 以上代码等同于下面的代码:
while (*p) {
cout << *p++ << '\n'; // 输出当前字符,指针同时移动
}
指针"作为"数组
- 对于字符串声明指针,它会指向第一个字符作为
ptr[0]。 - 指针可以作为数组调用,所以在例子中:
*(ptr) ≡ ptr[0] ≡ 'h'。 - 第一部分已经提到过,
s.c_str() ≡ &s[0](都属于 const char* 类型)
string s = "hello";
const char* ptr = s.c_str(); // *(ptr+2) == 'l'
数组"作为"指针
- 数组名在表达式环境会隐式转成指针,可以当做指针使用。比如
cstr[]衰减成指向首元素的指针&cstr[0],类型为 char*。 - 前者作为一次性分配不需要经过 Destructor,后者需要。
class StringA {
char cstr[256]; // 固定数组,赋予对象一块固定大小的栈空间,不需要额外分配
char* ptr = cstr; // 合法
};
class StringB {
char* ptr = new char[256]; // 动态分配堆上的空间,需要手动地动态分配/释放内存
};
数组不等价于指针
- 但数组 ≠ 指针。数组名是不可修改的 lvalue,你可以把数组衰减后赋给指针(1),却不能把指针赋 给数组(2)。
- 注意 arr 变成了指向首元素的指针,但 &arr 的类型是“指向整个数组的指针”。
- 如果
double arr[5]; double* dp = arr;,恒等式:arr[i] ≡ *(arr+i) ≡ *(dp+i) ≡ dp[i]。
int arr[4] = {10, 20, 30, 40};
int* ptr = arr; // 合法,数组名衰减为 int* (1)
int x = arr[2]; // 合法,等价于 *(arr + 2)或*(ptr + 2)
sizeof(arr); // 16(4 个 int 的总字节数)
sizeof(ptr); // 8(在 64 位机上指针通常 8 字节)
arr = ptr; // ❌ 不合法:数组不是可赋值对象 (2)
- 小注:lvalue 是可以放在赋值符号
=左侧的值或表达式,比如:解引用*ptr、变量int x等等。而不能写左边的就是 rvalue,一般是临时值或不能赋值的对象,比如:取地址&val、算术式x+y-1等等。
C 标准库拷贝函数 strncpy
在实现 String 类定义中的使用
char* strncpy(char* destination, const char* source, std::size_t num);
std::strncpy(this->c_str + this->strlen, other.c_str, copylength);
- 从 source 拷贝最多 num 个字符到 destination。如果 source 长度小于 num,会用 \0 填充剩余位置。
- 如果 source 长度 ≥ num,只会拷贝前 num 个字符,不会在末尾自动加 \0。
- 例子中将另一个字符数组连接至当前数组后 (这里的
this->都可以省略)。
完整例子比如 cpp 的 String 类需要定义 operation+= (连接两个字符串 ”str1“+”str2“):
void string::strcat(string const& other) { // "cat" whether the append operation will exceed MAXLEN 256
int total = strlen + other.strlen;
if (total >= MAXLEN) total = MAXLEN - 1; // exceeding 255, remaining char WILL be discarded
int copylen = total - strlen;
if (copylen > 0) {
std::strncpy(this->c_str + strlen, other.c_str, copylen);
strlen = total;
c_str[strlen] = '\0'; // in case of exceeding
}
}
// ...... //
string& string::operator+=(string const& other) { strcat(other); return *this; }
- 代码段将当前实例 string 末尾接上 另一个实例 other,去掉超出内存部分,并确保末尾是 ‘\0’,这样这个代码就可以被递归调用:
str::cout << string(str1) + string(str2) + string(str3) << '\n';
用指针自增实现 strncpy
#include <iostream>
using namespace std;
void str_cpy(char* dst, const char* src) {
while (*dst++ = *src++);
}
int main() {
char ptr1[] = "Hello";
char ptr2[100]; // buffered(要有足够缓冲栈)
str_cpy(ptr2, ptr1);
cout << ptr2 << '\n'; // 输出 Hello
return 0;
}
- 注意 while 中写的不是
==! 条件中不是判别式而是在赋值,其结果为当前被赋予给 dst[] 的字符,而非空字符/字符串都为真,直到遇到\0(也就是 0,也就是 False)才会结束循环。
<iostream> 与 <cstdio>
输出时保留小数
- CPP style: setprecision()
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double d = 3.141592653589793;
cout << "default + setprecision(2): " << setprecision(2) << d << '\n'; // 3.1(有效数字2位)
cout << fixed << setprecision(2);
cout << "fixed + setprecision(2): " << d << '\n'; // 3.14
cout << scientific << setprecision(2);
cout << "scientific + prec(2): " << d << '\n'; // 3.14e+00
cout << defaultfloat; // 恢复默认
}
- C style: printf()
#include <iostream>
#include <cstdio>
using namespace std;
double db = 3.14159265358979323846;
printf("This is a double: %.2f\n", db); // 相当于 fixed
(%d 整数,%c 字符,%s 字符串)
cout << fixed << setfill('0') << setw(10) << setprecision(4) << left << PI << endl;
FileStream
用于向文件输出或者从文件提取信息
- ifstream:默认
std::ios::in(只读模式) - outstream:默认
std::ios::out | std::ios::trunc(清空写入模式) - .open(cchar_file, mode):打开文件(和方式)
- .get(), .put():读和写
<fstream>不能学 Python 直接写“cin » s » outFile.txt"!!
流复制文件:
int main() {
ifstream fin;
ofstream fout;
fin.open("infile.txt"); // ”infile.txt".c_str()
fout.open("outfile.txt", std::ios::out | std::ios::app); // 追加写入模式
char ch;
while (fin.get(ch)) { // 每次读取一个字符,失败时(EOF)退出
fout.put(ch);
}
fin.close();
fout.flush();
fout.close();
return 0;
}
StringStream
- .str:将字符串流中的数据作为字符串取出
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main() {
int i;
cin >> i;
cout << "My favorite number is: " << i << endl; // Method1
string s = "My favorite number is: " + to_string(i); // Method2
cout << s << endl;
stringstream ss; // sstream Method
ss << "My favorite number is: " << i;
s = ss.str();
cout << s << endl;
内存分配
- 所有通过新增对象语法新建的数据 (比如
int* a = cstr) 都是储存在栈上的局部变量,在函数返回时自动销毁。通常比如 main 方法结束后销毁。 - 所有通过
new声明的新数据 (比如int* a = new int[]) 都是储存在堆上的动态分配的变量,需要在结束生命周期时手动用delete / delete[]销毁,防止内存泄漏。简而言之,代码里出现多少次 “new",就应该对应出现多少次 ”delete“。比如:
int* indexArr(int n) {
int* arr = new int[n];
for (int i = 0; i < n; i++) arr[i] = i;
return arr;
}
int main() {
int* digits = indexArr(10);
delete[] digits; // 必须释放
return 0;
}
const 和 static
const的三种用法:
const double PI = 3.14159; // 无论如何无法修改该变量
void print(const string& s); // 函数不能修改该变量
int size() const; // 函数不能修改对象(对象的成员变量)
static是属于类的公用变量,由所有对象共享:
class MyClass {
public:
static int count; // 声明一个静态变量
MyClass() { count++; }
};
int MyClass::count = 0; // 必须在类外定义并初始化
int main() {
MyClass a;
MyClass b;
cout << MyClass::count << endl; // 输出 2
}