C++ Primer
Hello world
Clang 是苹果官方的用来编译 C 家族的编译器,它是 LLVM 的一部分。相比于 Xcode 5 版本前使用的 GCC 有多项优化。
编译:clang++ main.cpp
编译 C++11:clang++ -std=c++11 main.cpp
文件重定向:./a.out <infile >outfile
<< 是输出运算符,>> 是输入运算符。
endl 是操纵符 (manipulator),写入 endl 的效果是结束当前行,并将设备关联的 buffer 中的内容刷新到设备中。缓冲刷新操作可以保证内存中的数据都真正写入到输出流中。
:: 是作用域运算符。
#include <iostream>
int main() {
int sum = 0, value = 0;
// 以 istream 对象的状态作为检测条件
// 当遇到 end-of-file (ctrl+D) 或无效输入(输入的值不是一个整数),istream 状态就会变为无效
while (std::cin >> value) {
sum += value;
}
std::cout << "Sum is " << sum << std::endl;
return 0;
}
编译并链接:clang++ main.cpp fact.cpp,产出 a.out 文件。
等价于下面的步骤(分离式编译):
- 只编译:
clang++ -c main.cpp,产出main.o文件。 - 只编译:
clang++ -c fact.cpp,产出fact.o文件。 - 链接:
clang++ main.o fact.o,产出a.out文件。
## is called token concatenation, used to concatenate two tokens in a macro invocation.
#define Flutter_CONCAT2(A, B) A##B
#define Flutter_CONCAT(A, B) Flutter_CONCAT2(A, B)
从标准输入流中获取每一行:
string line;
while (getline(cin, line))
if (!line.empty())
cout << line << endl;
变量和基本类型
溢出
int main(int argc, const char * argv[]) {
// char 可以表示 [-128, 127] 之间的数
char c1 = -128;
printf("%d\n", c1); // -128 (0x80, 0b10000000)
char c2 = 127;
printf("%d\n", c2); // 127 (0x7f, 0b01111111)
char c3 = -129;
printf("%d\n", c3); // 127 (0x7f, 0b01111111)
char c4 = 128;
printf("%d\n", c4); // -128 (0x80, 0b10000000)
return 0;
}
模除
模除(又称模数、取模):The modulo operator, denoted by %, produces the remainder of an integer division.
Modulo Operator (%) in C/C++ with Examples - GeeksforGeeks
模除的两个操作数都只能是整型,不能是浮点类型。
对于操作数为负数的情况,模除结果的符号取决于机器,因为该操作是下溢或上溢的结果。
int main(int argc, const char * argv[]) {
// 正整数模除
printf("%d\n", 3 % 4); // 3
printf("%d\n", 4 % 3); // 1
printf("%d\n", 4 % 2); // 0
// 负数模除
printf("%d\n", -3 % 4); // -3
printf("%d\n", 4 % -2); // 0
printf("%d\n", -3 % -4); // -3
return 0;
}
基本内置类型
无符号类型中所有位都用于存值;有符号类型,C++标准并没有规定如何存储,但约定了负值与正值间尽量平衡。
关于类型转换:
- 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为 0 则结果为 false,否则结果为 true。
- 当我们把一个布尔值赋给非布尔类型时,初始值为 false 则结果为 0,初始值为 true 则结果为 1。
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数的模除。例如,8 位的
unsigned char可以表示 256 个数。如果我们赋了一个区间以外的值,则实际的结果是该值对 256 取模后的余数。因此,把 -1 赋给unsigned char所得的结果是 255。 - 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的 (undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
int main(int argc, const char * argv[]) {
bool b = -1;
printf("%s\n", b ? "true" : "false"); // true
unsigned char c = -1;
printf("%d\n", c); // 255(0xFF,所有位都是1)
return 0;
}
切勿混用带符号类型和无符号类型!!
unsigned u = 0;
int i = -1;
std::cout << u + i << std::endl; // 4294967295,2^32 = 4294967296
每个字面值常量 (literal) 都对应一种类型,字面量的形式和值决定了它的数据类型。
整型:
// 下面三个数都表示 20
int a = 20; // 十进制
int a = 024; // 0 开头的整数表示八进制数
int a = 0x14; // 0x 或 0X 开头的整数表示十六进制数
// 最小匹配类型就是能容纳这个字面量的、尺寸最小的类型
int a = 10; // 最小匹配类型 int
unsigned int a = 10u; // 最小匹配类型 unsigned
long a = 10L; // 最小匹配类型 long
unsigned long a = 10UL; // 最小匹配类型 unsigned long
C++ 14 标准里新增了二进制类型:
int x = 0b00010000;
浮点型:
double b = 3.14; // 浮点型默认是 double
float b = 3.14f;
long double b = 3.14L;
double b = 10.;
double b = 10e-2; // 指数部分用 e 或 E 表示
long double b = 3.14e0L;
字符和字符串字面量:
char c = 'a'; // 字符
wchar_t c = L'a'; // 宽字符
char s[] = "a"; // 字符串字面量的类型实际上是字符构成的数组
cout << strlen(s) << endl; // 1
// 对于 C 数组,C++ 没有提供方法直接计算其长度,可以借助 sizeof()、begin()、end() 间接计算其长度。
// 编译器会在字符数组结尾添加一个空字符 '\0',因此数组长度会比它的内容多 1
int length = sizeof (s) / sizeof (s[0]);
cout << length << endl; // 2
int length2 = std::end(s1) - std::begin(s1);
cout << length2 << endl; // 2
wchar_t s[] = L"abc"; // 宽字符数组
转义字符:
// \ 后面跟着八进制数字、\x 后面跟着十六进制表示转义字符
char c = '\10';
char c = '\x4d';
变量
定义一个变量并初始化:
// C++98
int units_sold1 = 1;
int units_sold2 = {2};
int units_sold3(3);
// C++11
int units_sold4{4}; // 用花括号初始化的形式称为列表初始化 (list initialization)
列表初始化时如果存在丢失信息的风险,编译器会报错:
long double ld = 3.1415926;
int a(ld), b = ld; // 可以编译
int c{ld}, d = {ld}; // ❌ 报错
函数体内的内置类型变量如果没有初始化,则其值未定义;类的对象没有显式初始化,其值由类的默认构造函数确定。
分离式编译指的是允许将程序分为多个文件,每个文件独立编译。为了支持分离式编译,C++ 将声明 (declaration) 与定义 (definition) 区分开。变量的定义只能出现在一个文件中,其它用到该变量的文件必须对其进行声明。
extern int i; // 声明 i
int j; // 声明并定义 j
作用域可以嵌套,外层的叫 outer scope,内层的叫 inner scope。
void test() { // 名字 test 拥有全局作用域 (global scope)
int sum = 0; // 名字 sum 拥有块作用域 (block scope)
}
引用
目前我们接触到的变量声明,由一个基本数据类型 + 一个变量名组成。现在我们学习更复杂的声明,它基于基本数据类型得到更复杂的类型。
引用即别名,是已存在的对象的另一个名字。引用必须被初始化!定义引用时,程序把引用和它的初始值绑定在一起。无法令引用重新绑定到另一个对象!
int ival = 1024;
int &refVal = ival; // ✅ refVal 指向 ival(是 ival 的另一个名字)
int &refVal2; // ❌ 引用必须被初始化!
int &refVal3 = refVal // ✅ 将 refVal3 也绑定到 ival 上
定义了引用之后,操作引用就是操作它绑定的对象!
refVal = 2048;
std::cout << ival << std::endl; // 2048
int i = 10, &r1 = i;
double d = 3.14, &r2 = d;
r2 = r1; // 等价于 d = i;
std::cout << d << std::endl; // 10
r1 = 20;
r2 = 31.4;
std::cout << i << " " << d << std::endl;
引用本身不是一个对象,不能定义引用的引用。
引用必须绑定一个对象,不可以绑定一个 literal 或者表达式的计算结果。
int &ref2 = 10; // ❌
int &ref3 = 10 + 10; // ❌
指针
A reference refers to another type. A pointer points to another type. 引用本身不是一个对象;指针本身是一个对象,允许对指针进行赋值和拷贝。
引用一旦定义,就绑定了它初始化的对象,无法令其绑定到其它的对象;指针无须在定义时初始化,可以随时给它赋值一个新的地址,指向一个新的对象。
指针用于存放某个对象的地址。获取对象的地址,用取地址符 &。指针的类型,要和它指向的对象类型严格匹配!
int ival = 42;
int *pt = &ival; // pt 存放 ival 的地址,或者说 pt 是指向 ival 的指针
如果指针指向了一个对象,那么可以使用解引用符 * 来访问该对象。
std::cout << *pt << std::endl;
& 出现在声明中,代表引用;出现在表达式中,代表取地址符!
* 出现在声明中,代表指针;出现在表达式中,代表解引用符!
int i = 10; // 声明并定义 i,并初始化它的值为 10
int &r = i; // r 是一个引用,即 i 的一个别名
int *p; // p 是一个指向 int 类型的指针
p = &i; // 取 i 的地址,赋值给 p,p 指向 i
*p = i; // 将 i 的值赋值给 p 指向的对象
int &r2 = *p; // 定义引用类型 r2 并绑定到 p 指向的对象
int j = 42, *p2 = &j;
int *&pref = p2; // pref is a reference to the pointer p2
// prints the value of j, which is the int to which p2 points
std::cout << *pref << std::endl;
// pref refers to a pointer; assigning &i to pref makes p point to i
pref = &i; // 引用不可以绑定到别的对象,但指针可以随时改变指向
std::cout << *pref << std::endl; // prints the value of i
空指针不指向任何对象,在试图使用一个指针之前,应该检查它是否为空。NULL 是预处理变量 (preprocessor variable),定义在 cstdlib,它的值就是 0;预处理器 (preprocessor) 是运行于编译之前的一段程序。在现行标准下,最好使用 nullptr,避免使用 NULL。
int *p1 = nullptr; // literal
int *p2 = 0; // literal
int *p3 = NULL;
0 指针的条件值是 false;非 0 指针的条件值是 true。
int *pi1 = 0;
int *pi2 = &ival;
if (!pi1) {
std::cout << pi1 << std::endl; // 0x0
}
if (pi2) {
std::cout << pi2 << std::endl;
}
void * 是一种特殊的指针,它可以存放任意对象的地址。
double obj = 3.14;
void *pv = &obj;
指针的指针:指针是内存中的对象,和其它对象一样也有自己的地址,因此可以将指针的地址再存放到另一个指针中。
int **ppi = &pi2; // ppi -> pi2 -> ival
const 限定符
const 修饰的变量,其值不可改变。
默认状态下,const 对象仅在本文件内有效。想在多个文件之间共享 const 对象,必须在变量定义前添加 extern 关键字。
#include <iostream>
const int bufSize = 512; // 本文件内有效
extern const int bufSize2 = 512;
int main() {
return 0;
}
const 对象必须初始化:const int k; // ❌
Reference to const
绑定到常量对象需要声明 reference to const:
const int ci = 1024;
const int &r1 = ci;
r1 = 42; // ❌ ci 是一个常量,不能被修改
int &r2 = ci; // ❌ 普通引用不能绑定到常量对象
初始化 reference to const 时允许用任意表达式作为初始值,只要该表达式可以转换为引用的类型。
int i = 42;
const int &r1 = i;
const int &r2 = 42; // ✅ 允许 const int& 绑定到字面值
const int &r3 = r1 * 2; // ✅
注意引用绑定中的隐式转换。
double dval = 3.14;
const int &r6 = dval; // 由于 r6 的类型是整数,实际上绑定到了一个临时对象 const int temp = dval;
dval = 10.24;
std::cout << r6 << std::endl; // 3
允许 reference to const 引用非常量对象:
int i = 42;
const int &r = i; // ✅ 允许 const int& 绑定到普通 int 对象上,但不能通过 r 修改变量 i 的值
i = 84; // 虽然不能通过 r 修改变量 i 的值,但 i 毕竟不是常量,可以通过其它途径修改
std::cout << r << std::endl; // 84
Pointer to const, const pointer
要想存放常量对象的地址,只能使用指向常量的指针 (pointer to const):
const double pi = 3.14;
const double *cptr = π
允许 pointer to const 指向非常量,pointer to const 只是要求不能通过本指针改变所指对象的值,但那个所指对象的值可以通过其他途径改变。
double dval = 3.14;
cptr = &dval;
dval = 10.0
const pointer 是不会改变指向的指针,它一旦初始化,就永远指向那个对象。const pointer 必须被初始化。
int errNumb = 0;
int *const curErr = &errNumb;
const double pi2 = 3.14159;
const double *const pip = &pi2; // 弄清楚声明的含义,最好的方式是从右向左读:pip is a const pointer to const double
Top-level const and low-level const
const pointer (top-level const): 指针本身是一个常量。
pointer to const (low-level const): 指针所指的对象是一个常量。
更一般地,顶层 const 可以表示任意数据类型的对象是常量;底层 const 则与指针、引用等复合类型的基本类型部分有关。
int i = 0;
int *const p1 = &i; // top-level const
const int ci = 42; // top-level const
const int *p2 = &ci; // low-level const
const int &r = ci; // low-level const
const int *const p3 = p2; // top-level and low-level const
它们的区别主要体现在拷贝操作时。top-level const 不存在限制;low-level const 则存在一些限制。非常量可以转为常量,反之则不行。
int *p = p2; // ❌
int &r2 = ci; // ❌
constexpr
常量表达式是指在编译过程中就能计算出结果的表达式。
C++11 允许将变量声明为 constexpr 以便编译器验证变量值是否是一个常量表达式。
如果你肯定变量是一个常量表达式,那就把它声明成 constexpr 吧!
constexpr int mf = 20;
constexpr int limit = mf + 1;
声明 constexpr 时用到的类型是字面值类型 (literal type),算术类型、指针、引用、字面值常量类、枚举类型都属于字面值类型;自定义的类、IO 库、string 类型不是字面值类型。
函数体内的变量并非存放在固定地址,因此 constexpr 指针不能指向这样的变量。定义于所有函数体之外的对象,其地址固 定不变,可以用来初始化 constexpr 指针。
constexpr 修饰的指针是常量指针,指的是编译期就能够知道这个指针指向哪里。constexpr 指针既可以指向常量也可以指向非常量。
const int *p = nullptr; // p 是 pointer to const
constexpr int *q = nullptr; // q 是 const pointer
类型别名
typedef double wages;
typedef double *dptr;
typedef char *pstring;
wages i = 3.14;
dptr p = &i;
char c = 'c';
pstring cstr = &c;
编写头文件
头文件通常包含那些只能被定义一次的实体,比如 class, struct, const, constexpr。
头文件被多次包含(显式或隐式)会带来问题,确保头文件被多次包含仍能安全工作的技术是 preprocessor。
整个程序中的预处理变量必须唯一,通常做法是用头文件的名字大写来保证唯一性。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
// 类所在的头文件名字应与类一样
struct Sales_data {
std::string bookNo;
unsigned units_bold = 0; // C++11 in-class initializer
double revenue = 0; // C++11 in-class initializer
};
#endif