MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++变量与基本类型详解

2021-09-077.7k 阅读

C++ 变量概述

在 C++ 编程中,变量是一个核心概念。变量可以看作是内存中用于存储数据的一个命名位置。当我们在程序中声明一个变量时,实际上是在内存中为该变量分配了一定的空间,这个空间的大小取决于变量的数据类型。

变量的声明与定义

变量的声明向编译器介绍变量的类型和名称,但并不一定为变量分配内存空间。而变量的定义不仅声明了变量,还为其分配了内存空间并可以进行初始化。

例如,以下是变量声明:

extern int a; // 声明一个整型变量 a,但未分配内存

而这是变量定义:

int b = 10; // 定义一个整型变量 b,并初始化为 10,同时分配内存

在实际编程中,一个变量可以多次声明,但只能在一个地方定义。

变量的命名规则

C++ 中变量命名需要遵循一定规则:

  1. 只能包含字母、数字和下划线:例如 my_variablevar1 是合法的,而 my-var 是不合法的,因为 - 不是允许的字符。
  2. 不能以数字开头:如 1var 是不合法的,而 var1 是合法的。
  3. 区分大小写MyVarmyvar 是两个不同的变量。
  4. 不能使用关键字:C++ 有许多关键字,如 intifwhile 等,这些不能作为变量名。

C++ 基本数据类型

C++ 提供了丰富的基本数据类型,这些类型是构建复杂数据结构和程序逻辑的基础。基本数据类型可分为以下几类:整数类型、浮点类型、字符类型和布尔类型。

整数类型

整数类型用于表示整数数值,它们在内存中占用不同的字节数,这决定了它们能够表示的数值范围。

整型分类

  1. short(短整型):通常占用 2 个字节。它能表示的范围大约是 -32,768 到 32,767。在一些系统中,它可能会被优化以提高特定场景下的处理效率。
short s = 100;
  1. int(整型):这是最常用的整数类型,一般占用 4 个字节(32 位系统)。它可以表示的范围约为 -2,147,483,648 到 2,147,483,647。
int i = 2000;
  1. long(长整型):通常占用 4 个字节,但在某些系统中可能占用 8 个字节。它能表示更大范围的整数,具体范围取决于系统。
long l = 1000000000L; // 后缀 L 表示 long 类型
  1. long long(长长整型):在 C++11 引入,保证至少占用 8 个字节,可表示非常大的整数。
long long ll = 9223372036854775807LL; // 后缀 LL 表示 long long 类型

有符号与无符号整数

上述整型默认是有符号的,即可以表示正数、负数和零。如果我们只需要表示非负整数,可以使用无符号类型。无符号类型在关键字前加上 unsigned

例如,unsigned int 可以表示 0 到 4,294,967,295 的范围。

unsigned int ui = 1000; // 无符号整型

无符号整数常用于需要表示数量、索引等场景,因为它不会出现负数情况,在某些计算中可以避免一些错误。

浮点类型

浮点类型用于表示带有小数部分的数值。由于计算机存储浮点数采用二进制近似表示,所以浮点数运算可能存在一定的精度误差。

浮点型分类

  1. float(单精度浮点型):占用 4 个字节,提供大约 6 - 7 位有效数字。适用于对精度要求不高且数据范围有限的场景。
float f = 3.14f; // 后缀 f 表示 float 类型
  1. double(双精度浮点型):占用 8 个字节,提供大约 15 - 17 位有效数字。这是最常用的浮点类型,适用于大多数需要高精度小数的计算。
double d = 3.141592653589793;
  1. long double(长双精度浮点型):占用 8 个字节或更多(取决于系统),提供更高的精度和更大的取值范围。
long double ld = 3.141592653589793116L; // 后缀 L 表示 long double 类型

字符类型

字符类型用于表示单个字符,在 C++ 中,字符类型本质上是整数类型的一种特殊形式,因为每个字符在内存中以对应的 ASCII 码值存储。

char 类型

char 类型占用 1 个字节,可以表示一个字符。它可以存储 ASCII 字符集中的字符,范围是 -128 到 127(有符号 char)或 0 到 255(无符号 char)。

char c = 'a';

我们可以对 char 类型进行数值运算,因为它本质上是一个整数。例如:

char c1 = 'a';
char c2 = c1 + 1; // c2 将是 'b' 的 ASCII 码值对应的字符

宽字符类型

对于非 ASCII 字符集(如中文、日文等),需要使用宽字符类型。C++ 提供了 wchar_tchar16_tchar32_t 等宽字符类型。

  1. wchar_t:它是一个宽字符类型,占用 2 个字节或 4 个字节(取决于系统)。用于表示宽字符,常用于处理 Unicode 字符。
wchar_t wc = L'中'; // 前缀 L 表示宽字符常量
  1. char16_t:在 C++11 引入,占用 2 个字节,专门用于表示 UTF - 16 编码的字符。
char16_t c16 = u'好'; // 前缀 u 表示 char16_t 类型的字符常量
  1. char32_t:同样在 C++11 引入,占用 4 个字节,用于表示 UTF - 32 编码的字符,能直接表示整个 Unicode 字符集。
char32_t c32 = U'世'; // 前缀 U 表示 char32_t 类型的字符常量

布尔类型

布尔类型用于表示逻辑值,只有两个取值:true(真)和 false(假)。在 C++ 中,布尔类型是 bool

bool b1 = true;
bool b2 = false;

布尔类型常用于条件判断语句,如 if - elsewhile 等,以控制程序的流程。例如:

int num = 10;
bool isGreater = num > 5;
if (isGreater) {
    std::cout << "The number is greater than 5" << std::endl;
}

变量的初始化与赋值

变量的初始化是在定义变量时给它赋予初始值的过程,而赋值是在变量已经定义后改变其值的操作。

初始化方式

  1. 直接初始化:直接在变量定义后使用括号或等号进行初始化。
int num1(10); // 使用括号
int num2 = 20; // 使用等号
  1. 列表初始化(C++11 引入):使用花括号进行初始化,这种方式可以避免一些类型转换问题,并且能更明确地表达初始化意图。
int num3{30};

列表初始化还可以用于初始化数组和结构体等复杂数据类型。

赋值操作

赋值操作通过赋值运算符 = 来实现。例如:

int a = 5;
a = 10; // 改变 a 的值

在进行赋值操作时,需要注意数据类型的兼容性。例如,将一个浮点型值赋给整型变量时,会发生截断,丢失小数部分。

int i;
float f = 3.14f;
i = f; // i 将变为 3

类型转换

在 C++ 编程中,经常需要在不同数据类型之间进行转换。类型转换可以分为隐式类型转换和显式类型转换。

隐式类型转换

隐式类型转换是由编译器自动进行的类型转换,通常发生在表达式中不同类型的操作数混合运算时。编译器会将低精度类型转换为高精度类型以避免数据丢失。

例如,在以下表达式中:

int i = 5;
float f = 3.14f;
float result = i + f; // i 会被隐式转换为 float 类型

在这个例子中,int 类型的 i 被隐式转换为 float 类型,以便与 f 进行加法运算。

然而,隐式类型转换也可能带来一些问题。例如,将高精度类型转换为低精度类型时会发生数据截断。

double d = 10.5;
int j = d; // d 被截断为 10,小数部分丢失

显式类型转换

显式类型转换是程序员通过特定的语法明确指定的类型转换。C++ 提供了四种类型转换运算符:static_castdynamic_castconst_castreinterpret_cast

static_cast

static_cast 用于具有明确定义的类型转换,例如基本数据类型之间的转换,以及类层次结构中基类和派生类指针或引用之间的转换(上行转换是安全的,下行转换需要谨慎)。

float f = 3.14f;
int i = static_cast<int>(f); // 将 float 转换为 int,截断小数部分

class Base {};
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 上行转换安全

dynamic_cast

dynamic_cast 主要用于在运行时进行类型检查和安全的向下转型(从基类指针或引用转换为派生类指针或引用)。它只能用于含有虚函数的类层次结构中。如果转换失败,dynamic_cast 对于指针返回 nullptr,对于引用抛出 std::bad_cast 异常。

class Base {
    virtual ~Base() {}
};
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
    // 转换成功
} else {
    // 转换失败
}

const_cast

const_cast 用于去除变量的 constvolatile 限定符。通常用于在特定情况下需要修改 const 对象的场景,但这种做法应该谨慎使用,因为它打破了 const 的语义。

const int num = 10;
int* ptr = const_cast<int*>(&num);
*ptr = 20; // 虽然可以修改,但这种操作破坏了 const 语义

reinterpret_cast

reinterpret_cast 用于进行高度危险的类型转换,它几乎可以将任何类型转换为其他类型,通常用于底层指针操作或与硬件相关的编程。这种转换不进行任何类型检查,可能导致未定义行为。

int num = 10;
char* charPtr = reinterpret_cast<char*>(&num);

变量的作用域与生命周期

变量的作用域决定了变量在程序中的可见性和可访问范围,而变量的生命周期则描述了变量在内存中存在的时间段。

作用域分类

  1. 局部作用域:在函数内部定义的变量具有局部作用域,它们只能在函数内部访问。当函数结束时,这些变量会被销毁,释放其所占用的内存。
void myFunction() {
    int localVar = 10;
    // 这里可以访问 localVar
}
// 这里不能访问 localVar,因为它已超出作用域
  1. 块作用域:在一对花括号 {} 内定义的变量具有块作用域。例如,在 ifwhilefor 语句块中定义的变量,只能在该块内访问。
if (true) {
    int blockVar = 20;
    // 这里可以访问 blockVar
}
// 这里不能访问 blockVar
  1. 全局作用域:在所有函数外部定义的变量具有全局作用域,它们在整个程序中都可以访问(需要注意命名冲突问题)。全局变量在程序启动时创建,在程序结束时销毁。
int globalVar = 30;
void anotherFunction() {
    // 这里可以访问 globalVar
}

生命周期

  1. 自动变量:具有局部作用域的变量通常是自动变量,它们在进入其作用域时创建,离开作用域时销毁。例如函数内部定义的局部变量就是自动变量。
void func() {
    int autoVar = 40;
    // autoVar 的生命周期从这里开始
}
// autoVar 的生命周期在这里结束
  1. 静态变量:静态变量分为局部静态变量和全局静态变量。局部静态变量在函数内部定义,使用 static 关键字修饰。它在程序启动时创建,并且在函数多次调用之间保持其值。全局静态变量在所有函数外部定义,使用 static 关键字修饰,其作用域仅限于定义它的文件,生命周期与程序相同。
void staticFunc() {
    static int localStaticVar = 0;
    localStaticVar++;
    std::cout << "Local static var: " << localStaticVar << std::endl;
}
// 全局静态变量
static int globalStaticVar = 100;
  1. 动态分配变量:通过 new 运算符分配的变量在堆上,其生命周期需要程序员手动管理,通过 delete 运算符来释放内存。如果不及时释放,会导致内存泄漏。
int* dynamicVar = new int(50);
// dynamicVar 的生命周期从这里开始
delete dynamicVar;
// dynamicVar 的生命周期在这里结束

常量与限定符

常量是在程序执行过程中其值不能被改变的量,C++ 提供了多种方式来定义常量,并通过限定符来控制变量的属性。

常量定义

  1. const 常量:使用 const 关键字定义常量,一旦初始化后,其值就不能被修改。
const int num = 10;
// num = 20; // 这将导致编译错误

const 常量可以用于函数参数,以表明函数不会修改该参数的值。

void printConst(const int value) {
    std::cout << "Const value: " << value << std::endl;
}
  1. constexpr 常量表达式:在 C++11 引入,constexpr 用于声明常量表达式,即其值在编译时就能确定。这使得编译器可以在编译期进行一些计算,提高效率。
constexpr int result = 2 + 3;

constexpr 函数也可以用于返回常量表达式,例如:

constexpr int add(int a, int b) {
    return a + b;
}
constexpr int sum = add(4, 5);

限定符

  1. const 限定符:除了用于定义常量,const 限定符还可以用于指针和引用,以限制对所指向对象或引用对象的修改。
int num1 = 10;
const int* constPtr = &num1; // constPtr 不能指向其他地方,其所指向的值也不能通过 constPtr 修改
int* const ptrConst = &num1; // ptrConst 不能指向其他地方,但可以通过 ptrConst 修改其所指向的值
  1. volatile 限定符volatile 用于告诉编译器,该变量的值可能会在程序控制之外被改变,例如硬件寄存器的值。这防止编译器对该变量进行过度优化,确保每次访问该变量时都从内存中读取实际值。
volatile int hardwareRegister;
// 每次访问 hardwareRegister 时,编译器都会从内存读取实际值

通过深入理解 C++ 的变量与基本类型,包括变量的声明、定义、初始化、类型转换、作用域、生命周期以及常量和限定符等概念,程序员能够更准确地编写高效、健壮的 C++ 程序。在实际编程中,合理选择数据类型和运用变量相关特性是优化程序性能和避免错误的关键。同时,随着 C++ 标准的不断演进,新的特性和语法也在不断丰富这些概念,需要开发者持续学习和关注。