C++证道之路第二章变量和基本类型

发布于:2024-07-09 ⋅ 阅读:(152) ⋅ 点赞:(0)

一、基本内置类型

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开头的代表十六进制数。

浮点数字面值

浮点数字面值用于表示实数,可以是floatdoublelong double类型。它们可以通过以下方式表示:

没有后缀,默认为double类型:如 3.14 或 3.14e2

后缀fF表示float类型:如 3.14f

后缀lL表示long double类型:如 3.14L

字符字面值

字符字面值用于表示单个字符,它们被包含在单引号内,如 'a''7'

字符串字面值

字符串字面值用于表示字符序列,它们被包含在双引号内,如 "Hello, world!"。字符串字面值同样支持上述的转义序列。

布尔字面值

布尔字面值有两个可能的值:truefalse

字符字面值的转义序列

字符字面值中可以包含转义序列,用于表示特殊字符:

\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函数不能在编译期求值,编译器会产生一个错误。

constconstexpr都是为了增加代码的安全性和性能而设计的,但是它们的使用场景和目的不同。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文件中,这样可以进一步分离接口和实现,便于代码的组织和维护。


网站公告

今日签到

点亮在社区的每一天
去签到