一、基本内置类型
C++定义了一套包括算数类型和空类型在内的基本数据类型。其中算数类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊场合。
1.算数类型
算数类型分为两类:整型(包括字符和布尔类型在内)和浮点型。
带符号类型和无符号类型:
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
类型int、short、long和 long long 都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int可以缩写为unsigned。
2.类型转换
类型所能表示的值的范围决定了转换的过程:
当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false否则结果为true。
当我们把一个布尔值赋给非布尔类型时,初始值为false 则结果为0,初始值为true则结果为1。
当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
提示:
切勿混用带符号类型和无符号类型。如果表达式里既带有符号类型又带有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动的转换成无符号数。
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。例如,在一个形如 a*b 的式子中,如果a=-1, b=1,而且a和b都是int,则表达式的值显然为-1。然而,如果a是int,而b是unsigned,则结果须视在当前机器上int所占位数而定。
3.字面值常量
整数字面值
我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0x开头的代表十六进制数。
浮点数字面值
浮点数字面值用于表示实数,可以是float
、double
或long double
类型。它们可以通过以下方式表示:
没有后缀,默认为double
类型:如 3.14
或 3.14e2
后缀f
或F
表示float
类型:如 3.14f
后缀l
或L
表示long double
类型:如 3.14L
字符字面值
字符字面值用于表示单个字符,它们被包含在单引号内,如 'a'
或 '7'
。
字符串字面值
字符串字面值用于表示字符序列,它们被包含在双引号内,如 "Hello, world!"
。字符串字面值同样支持上述的转义序列。
布尔字面值
布尔字面值有两个可能的值:true
和 false
。
字符字面值的转义序列
字符字面值中可以包含转义序列,用于表示特殊字符:
\n
表示换行
\t
表示水平制表符
\r
表示回车
\b
表示退格
\a
表示警报(响铃)
\v
表示垂直制表符
\'
表示单引号
\"
表示双引号
\\
表示反斜杠
\?
表示问号
\xHH
表示十六进制的字符(HH是两位十六进制数)
\uUUUU
和 \UUUUUUUU
分别表示Unicode编码的16位和32位字符
原始字符串字面值
原始字符串字面值允许包含任何字符,甚至包括换行和引号,它们使用R"(raw_string)"
的形式表示。
用户定义的字面值
从C++11开始,可以定义自己的字面值后缀,用于创建特定类型的字面值。例如,你可以定义一个字面值后缀_ft
来创建一个特定类型的浮点数,或者定义一个后缀_str
来处理特定的字符串格式。
用户定义的字面值需要通过命名空间作用域的模板函数来定义,这个函数的名字必须以operator "" _<literal_suffix>
的形式出现。例如:
namespace literals {
template <typename T>
T operator "" _ft(const char* str, size_t len) {
// 解析str到T类型并返回
}
}
auto value = 123_ft; // 使用用户定义的字面值后缀_ft
二、变量
1.变量定义
变量定义是指在C++中创建一个变量,它不仅声明了变量的类型和名称,而且还为其分配了内存空间。定义通常会伴随着初始化,即给变量设定一个初始值。变量定义的基本语法如下:
type identifier; // 不初始化
type identifier = value; // 初始化
2.变量声明和定义的关系
变量声明和定义在C++中有明显的区别:
声明(Declaration):仅告知编译器变量的存在及其类型,但不分配内存。声明可以在多个地方进行,只要至少有一个地方伴随着定义。
定义(Definition):除了声明外,还会为变量分配内存空间,并且可以初始化变量。每个变量在程序中只能有一个定义。
例如,变量的声明和定义可以分开:
// 声明
extern int myVar;
// 定义
int myVar = 10;
3.标识符
标识符是在C++中用来命名变量、函数、类、数组等的名称。标识符遵循以下规则:
必须以字母或下划线开头。
可以由字母、数字和下划线组成。
区分大小写。
不应与C++的关键字重名。
例如,myVariable
, _myVariable
, MyVariable
都是合法的标识符。
4.名字的作用域
作用域决定了标识符在何处可被访问。C++中的主要作用域类型包括:
局部作用域(Local Scope):变量只在其被定义的函数或块中可见。
全局作用域(Global Scope):变量在整个程序中都可见,除非被局部作用域中的同名变量隐藏。
文件作用域(File Scope):变量在一个源文件中可见,但不在其他文件中可见,除非有外部链接性。
块作用域(Block Scope):变量在其定义的代码块内可见,例如在if语句或循环体内。
例如,局部变量和全局变量的区别:
int globalVar = 10; // 全局变量
void myFunction() {
int localVar = 20; // 局部变量
// 在这里可以访问localVar和globalVar
}
int main() {
// 在这里只能访问globalVar
return 0;
}
三、复合类型
1.引用
引用是一种特殊的指针,它看起来像一个别名,指向另一个变量的地址,但它不像普通指针那样可以被重新赋值为指向其他变量。一旦一个引用初始化为一个变量,它就永久绑定到该变量上。
声明引用
引用的声明语法是在类型后面加上&
符号。例如:
int x = 10;
int& ref = x; // ref 是 x 的引用
这将创建一个名为ref的引用,它绑定到了x上。此后,对ref的任何操作都会影响到x。
声明复合类型的引用
对于复合类型,如数组或对象,可以声明引用:
class MyClass {
public:
int data;
};
MyClass obj;
MyClass& refObj = obj; // refObj 是 obj 的引用
2.指针
指针是一个变量,它可以存储一个内存地址。指针可以指向各种数据类型,包括基本类型和复合类型。
声明指针
指针的声明使用*
符号。例如:
int* pInt; // pInt 是一个指向 int 类型的指针
声明复合类型的指针
复合类型的指针,如指向数组或对象的指针:
int arr[5];
int (*pArr)[5] = &arr; // pArr 是一个指向 int 数组的指针
MyClass* pObj; // pObj 是一个指向 MyClass 对象的指针
3.理解复合类型的声明
理解复合类型的声明可能会有些复杂,尤其是当涉及到多重指针或指针引用时。关键在于理解*
和&
符号的位置。
当*
紧跟着类型时,它表示一个指向该类型的指针。
当*
在标识符前,它表示一个指针变量。
当&
出现在类型后面,它表示一个引用。
int** pPtr; // pPtr 是一个指向 int 指针的指针
int& refToInt; // refToInt 是一个指向 int 的引用
int (*pFunc)(int); // pFunc 是一个指向接受 int 参数并返回 int 的函数的指针
四、const限定符
const
关键字在C++中用于指定一个变量或对象的某个方面是不可修改的。这可以增强代码的安全性和效率。
1.const的引用
const
引用允许你引用一个对象,但是不允许通过该引用修改对象的内容。这对于传递大对象作为函数参数非常有用,因为它可以避免复制对象的成本,同时确保函数不会无意中修改对象。
void print(const std::string& str) {
std::cout << str << std::endl;
}
std::string s = "Hello, World!";
print(s); // s 不会被修改
2.指针和const
const
可以与指针结合使用,以限制指针可以修改的部分。const
可以应用于指针本身(不能改变指针指向的地址),或者应用于指针所指向的对象(不能改变对象的值)。
int value = 10;
const int* pConstValue = &value; // pConstValue 指向的值不能被修改
int* const pConst = &value; // pConst 指向的地址不能被修改
const int* const pBothConst = &value; // pBothConst 既不能改变地址也不能改变值
3.顶层const
顶层const和底层const指的是const
修饰符在指针或引用层次结构中的位置。顶层const是指最外层的const
,底层const是指更深层的const
。
int value = 10;
int* const pTopConst = &value; // 顶层const,pTopConst不能指向其他地方
const int* pBottomConst = &value;// 底层const,*pBottomConst不能被修改
4.constexpr和常量表达式
constexpr
关键字用于声明一个变量或函数的结果在编译时就可以计算出来。这意味着它可以在编译期就被求值,从而可以用于需要常量表达式的上下文,比如数组的大小、模板参数等。
constexpr int square(int x) { return x * x; }
constexpr int arrSize = square(5);
int arr[arrSize]; // arrSize 必须是常量表达式
使用constexpr
的函数必须保证在所有情况下都能在编译期间求值,这意味着函数内部不能有运行时依赖或副作用。如果一个constexpr
函数不能在编译期求值,编译器会产生一个错误。
const
和constexpr
都是为了增加代码的安全性和性能而设计的,但是它们的使用场景和目的不同。const
更多地用于防止运行时的修改,而constexpr
则用于优化编译期的代码生成。
五、处理类型
1.类型别名
类型别名允许你给已存在的类型起一个新的名字。这可以使得复杂的类型更加易读,并且能够减少代码中冗长类型的重复书写。类型别名可以通过typedef
(C++98)或using
(C++11及以后)来定义。
// 使用 typedef 定义类型别名
typedef std::vector<int> IntVector;
// 使用 using 定义类型别名 (C++11 及以后)
using IntVector = std::vector<int>;
IntVector v; // 等价于 std::vector<int> v;
2.auto类型说明符
auto
关键字让编译器自动推断变量的类型,只要初始化表达式的类型是明确的。这在处理模板、迭代器或复杂的表达式时特别有用,因为编译器通常能比程序员更快地确定类型。
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // it 的类型被推断为 std::vector<int>::iterator
auto sum = std::accumulate(v.begin(), v.end(), 0); // sum 的类型被推断为 int
3.decltype类型指示符
decltype
用于推导表达式的类型。它主要用于模板元编程中,以确定模板参数的实际类型。decltype
接受一个表达式或一个变量,并返回其类型。
int i = 10;
decltype(i) j = 20; // j 的类型为 int
std::vector<int> vec = {1, 2, 3};
decltype(vec)::iterator it = vec.begin(); // it 的类型为 std::vector<int>::iterator
int array[10];
decltype(array) arr2[5]; // arr2 的类型为 int[5]
六、自定义数据结构
1.定义Sales_data类型
定义一个简单的Sales_data
类,用于存储图书销售的数据,包括ISBN、销售数量、总收入和平均价格。
// sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
class Sales_data {
public:
std::string isbn() const { return bookNo; } // 返回ISBN
Sales_data& combine(const Sales_data&); // 更新当前对象的销售数据
Sales_data() = default; // 默认构造函数
Sales_data(std::string s) : bookNo(s) {} // 构造函数,接受一个ISBN
Sales_data(std::string s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(n * p) {} // 构造函数,接受ISBN、数量和单价
private:
std::string bookNo; // ISBN
unsigned units_sold = 0; // 销售数量
double revenue = 0.0; // 总收入
};
// 在类外部定义combine方法
Sales_data& Sales_data::combine(const Sales_data& rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
#endif // SALES_DATA_H
2.使用Sales_data类
接下来,我们创建一个简单的main.cpp
文件,使用Sales_data
类来演示其功能。
// main.cpp
#include <iostream>
#include "sales_data.h" // 引入自定义的头文件
int main() {
Sales_data item1("0-321-92365-1", 5, 19.99);
Sales_data item2("0-321-92365-1", 3, 19.99);
std::cout << "Before combining: " << item1.units_sold << ", " << item1.revenue << std::endl;
item1.combine(item2);
std::cout << "After combining: " << item1.units_sold << ", " << item1.revenue << std::endl;
return 0;
}
3.编写自己的头文件
sales_data.h
就是一个自定义的头文件。它包含了Sales_data
类的声明和实现。头文件使用了预处理器指令#ifndef
、#define
和#endif
来防止头文件被多次包含,这是C/C++中常见的防止重复包含的技术,被称为“include guards”。
通过这种方式,我们可以将Sales_data
类的实现细节封装起来,仅暴露必要的接口,保持代码的模块化和可维护性。在实际开发中,通常会将类的声明放在.h
或.hpp
文件中,而将其实现放在.cpp
文件中,这样可以进一步分离接口和实现,便于代码的组织和维护。