☰
C++基础入门教程(二):数据、变量、宏等
2026/10/20 4:31:12 网站建设 项目流程

前言


这是入门系列的第二篇,主题是「数据怎么表示、变量怎么定义、宏能做与不能做什么」。看起来都是最基础的内容,但恰恰是基础知识里的模糊地带最多:int到底几个字节、char有没有符号、const和constexpr差在哪、#define为什么被现代 C++ 极力劝退。这些问题在初学阶段若按「背结论」的方式处理,后面写出的代码会在跨平台时集中暴露。


必须先说明一点:C++ 标准不规定大多数基本类型的精确字节数,只规定最小范围和相对大小关系。sizeof(int)在常见平台上多为 4,但这是实现定义的结果,不是语言保证。char是否有符号同样是实现定义的。因此本文给出的尺寸一律标注为「常见平台」,遇到需要精确宽度的场合,应当使用<cstdint>里的std::int32_t这类定宽类型。


本文先讲类型与对象的基本概念,再讲变量的初始化方式,然后进入宏的世界:预处理器在编译之前做了什么、宏的典型陷阱、以及为什么现代 C++ 更推荐constexpr、inline函数和模板来替代宏。最后用一个综合示例把类型、变量、宏边界串起来。


一、基本数据类型


C++ 的基本类型分为几大类:布尔、字符、整数、浮点、void,以及由它们派生的指针和数组。下面按类别说明。




类型常见大小(常见平台)标准保证备注



bool1 字节至少能表示true/false参与算术运算时提升为int

char1 字节sizeof(char) == 1是否有符号由实现决定

signed char1 字节至少 -127 到 127明确有符号

unsigned char1 字节至少 0 到 255常用于字节缓冲

short2 字节至少 16 位

int4 字节至少 16 位最常见的默认整型

longWindows 4 / Linux 8至少 32 位LLP64 与 LP64 的差异点

long long8 字节至少 64 位C++11 起标准

float4 字节至少 6 位有效数字

double8 字节至少 10 位有效数字

long double8/12/16 字节至少与double同精度各平台差异很大



sizeof的结果类型是std::size_t,定义在<cstddef>。注意sizeof(char)恒为 1,但「1 字节」不等于「8 位」——标准只保证一个字节至少 8 位,实际平台几乎都是 8 位。


整数类型还有有符号与无符号之分。无符号运算按模 2^N 回绕,这是良定义的;有符号整数溢出则是未定义行为(undefined behavior, UB),不是「回绕成负数」。这一点必须记牢:int溢出后程序可能得到任何结果,包括看起来正常的结果,也可能被优化器利用而产生难以理解的代码。


字符类型要区分三个概念:char、signed char、unsigned char是三种不同的类型。char的符号性由实现决定,所以在把char当小整数用时,应先明确转换到signed char或unsigned char。wchar_t的宽度也是实现定义的:Windows 上通常 2 字节,Linux 上通常 4 字节。C++11 还引入了char16_t与char32_t用于 Unicode,C++20 增加了char8_t。


二、变量的声明、定义与初始化


「声明」告诉编译器某个名字存在及其类型;「定义」则真正分配存储或给出函数体。一个定义同时也是声明,但反之不成立。对变量而言,extern int x;是声明,int x;是定义。


C++11 起推荐使用列表初始化(花括号初始化),它有一个重要的附加效果:禁止窄化转换。


// 适用于 C++17
#include <vector>
#include <string>

int a = 0; // 拷贝初始化
int b(0); // 直接初始化
int c{0}; // 列表初始化(推荐)
int d{}; // 值初始化,d 为 0

double pi = 3.14159;
// int bad{pi}; // 错误:窄化转换,编译期报错
int ok = static_cast<int>(pi); // 显式转换可以

std::vector<int> v{1, 2, 3}; // 三个元素
std::vector<int> w(3); // 三个值为 0 的元素

对容器来说,v{1, 2, 3}与v(3)的差别尤其要注意:前者调用了接受std::initializer_list的构造函数,后者调用的是接受size_type的构造函数。这是列表初始化在容器上最容易踩的坑。


变量还有存储期(storage duration)和链接属性(linkage)两个维度。局部自动变量具有自动存储期,离开作用域即销毁;static局部变量具有静态存储期,首次执行到声明处时初始化(C++11 起该初始化是线程安全的);全局变量和命名空间作用域变量也具有静态存储期。链接属性决定名字能否跨翻译单元可见,static在命名空间作用域表示内部链接。


未初始化的局部变量具有不确定的值,读取它是 UB。这一点和某些语言「默认给零」的规则不同。全局变量和静态变量会被零初始化,但不要依赖这个规则来省事,显式初始化更清晰。


三、const、constexpr 与 auto


const表示「通过这个名字不能修改对象」,但它不保证对象本身不可变——别人可能通过别的路径修改。constexpr则表示「可以在编译期求值」,它隐含const,但语义更强。


// 适用于 C++17
const int runtime_value = compute(); // 运行期初始化,只读
constexpr int compile_time_value = 42; // 编译期常量

constexpr int square(int x) { return x * x; }
static_assert(square(4) == 16, "compile-time check");

constexpr函数在 C++11 中限制很多(函数体基本只能有一条 return),C++14 放宽了限制,允许循环、局部变量等。C++17 起constexprlambda 也可用。判断一个constexpr函数能否在编译期求值,取决于实参是否为常量表达式以及函数体是否满足要求;不满足时它仍然可以在运行期调用。


auto让编译器从初始化器推导类型。它有几个必须记住的规则:



  • auto x = expr;会丢弃顶层const和引用;

  • auto& x = expr;保留引用,const auto&保留只读引用;

  • auto&&是转发引用(在泛型语境中),能绑定左值和右值;

  • 花括号初始化与auto组合时,auto x{1}在 C++17 起推导为int,而auto x = {1}推导为std::initializer_list<int>。


decltype则保留表达式的完整类型(包括引用和const),decltype(auto)在 C++14 起用于返回值推导,能精确保留引用性。


四、宏:预处理器能做什么


宏由预处理器处理,发生在编译之前。预处理器只做文本替换,不理解类型、作用域、重载。这一点决定了宏的一切优点和缺点。


宏的常见用途有四类:



  1. 包含守卫:#ifndef/#define/#endif,或非标准的#pragma once;

  2. 常量:#define MAX_SIZE 1024;

  3. 函数式宏:#define SQUARE(x) ((x) * (x));

  4. 条件编译:#ifdef _WIN32之类。


预处理还提供了一批预定义宏,例如__FILE__(当前源文件名)、__LINE__(当前行号)、__DATE__、__TIME__、__cplusplus(标准版本号)。这些是排查问题和做条件编译的常用工具。C++20 起还有__VA_OPT__用于可变参数宏的可选展开,属于较新特性。


函数式宏最著名的陷阱是参数被求值多次和缺少括号:


// 适用于 C++17
#include <iostream>

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
int i = 3;
int r1 = SQUARE(i++); // 展开为 ((i++) * (i++)),行为未定义
int r2 = MAX(i++, 10); // i++ 可能执行两次
(void)r1;
(void)r2;
return 0;
}

SQUARE(i++)展开后同一个表达式里对i自增两次且没有顺序保证,C++17 之前是 UB,C++17 起对于*的操作数仍然没有顺序规定,依然不能这么写。MAX的问题更隐蔽:即使加了括号,a和b仍可能各求值两次,若实参有副作用就会出错。


宏还无法处理类型:MAX对int和std::string都能展开,但比较语义完全不同;宏也没有作用域,定义会一直影响到#undef或文件结束,容易与别处的同名宏冲突。


五、用现代设施替代宏


绝大多数宏的用途在现代 C++ 里都有更好的替代:




宏的用途现代替代说明



常量constexpr/inline constexpr有类型、有作用域、可调试

小型函数constexpr函数或inline函数参数只求值一次,支持重载

类型无关的比较函数模板 /std::max保留类型信息

条件编译尽量用if constexpr或构建系统平台判断仍需预处理器

调试输出日志库 /std::source_location(C++20)可获取文件名行号且类型安全

字符串化仍可用#运算符无完全等价替代



constexpr函数是替代函数式宏的首选:


// 适用于 C++17
#include <algorithm>

constexpr int square(int x) { return x * x; }

template <typename T>
constexpr const T& max_of(const T& a, const T& b) {
return (a < b) ? b : a;
}

max_of的参数是引用,只求值一次;模板保证类型一致;constexpr允许编译期使用。注意返回引用要求两个实参在返回后仍然存活,若传入临时对象就会悬垂,这是它和宏不同的地方,需要使用者注意。


包含守卫可以用#pragma once简化,但它不是标准语法,只是被主流编译器普遍支持。可移植的写法仍是传统的三行守卫。C++20 的模块(modules)从根本上改变了头文件的组织方式,但各编译器支持程度不一,初学阶段不必强求。


需要保留宏的场景主要有:平台条件编译、生成代码、日志/断言中需要__FILE__和__LINE__的地方、以及某些需要字符串化的接口。使用时要遵守两条纪律:宏名全大写并加项目前缀,所有参数和整体都加足括号。


六、综合示例


下面这个小程序把类型宽度、列表初始化、constexpr、static_assert和条件编译放在一起,展示各自的边界。


// 适用于 C++17
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>

constexpr int kBufferSize = 256;
static_assert(kBufferSize > 0, "buffer size must be positive");

struct Sample {
std::int32_t id{};
double value{0.0};
std::string name{};
};

int main() {
Sample s{1, 2.5, "alpha"}; // 聚合初始化

std::cout << "int bytes: " << sizeof(int) << "\n";
std::cout << "long bytes: " << sizeof(long) << "\n";
std::cout << "int max: " << std::numeric_limits<int>::max() << "\n";

unsigned int u = 0;
u = u - 1; // 无符号回绕,结果良定义
std::cout << "wrap: " << u << "\n";

#ifdef _WIN32
std::cout << "platform: windows\n";
#else
std::cout << "platform: other\n";
#endif

return 0;
}

逐项说明。std::int32_t来自<cstdint>,在支持的平台上精确为 32 位,比裸int更适合需要固定宽度的场景。Sample s{1, 2.5, "alpha"}是聚合初始化,成员按声明顺序对应;C++20 起还可以用指定初始化器写{.id = 1, .value = 2.5},但需要 C++20 及以上。u = u - 1在unsigned int上是模运算,结果等于该类型的最大值,这是标准保证的;如果u是int,同样写法就是 UB。#ifdef _WIN32是平台宏,属于实现定义的宏,不是 C++ 标准的一部分,只在对应平台上有意义。


常见坑点



  1. ❌ 认为int一定是 4 字节、long一定是 8 字节。


✅ 标准只规定最小范围;需要固定宽度用<cstdint>的定宽类型,需要平台相关尺寸用sizeof查询。



  1. ❌ 让有符号整数溢出,并期待它回绕成负数。


✅ 有符号溢出是 UB;需要回绕语义时使用无符号类型,或先做范围检查。



  1. ❌ 读取未初始化的局部变量。


✅ 定义时就初始化,或使用{}值初始化;开启编译器警告辅助发现。



  1. ❌ 在std::vector上用v{3}期待「三个元素」。


✅v{3}是一个元素 3,v(3)才是三个默认值;注意initializer_list构造的优先级。



  1. ❌ 写#define SQUARE(x) x*x并传入带副作用的实参。


✅ 用constexpr函数;若必须用宏,至少写成((x)*(x))并避免多次求值。



  1. ❌ 用#define定义常量,导致调试器看不到名字、作用域泄漏。


✅ 用constexpr或inline constexpr变量。



  1. ❌ 认为const对象一定在编译期确定、一定不可修改。


✅const只约束当前访问路径;编译期求值用constexpr,真正不可变可配合const接口设计。



  1. ❌ 把char当作确定的无符号字节类型。


✅ 字节缓冲用unsigned char或std::byte(C++17),文本用char。


总结




主题核心规则易错点



基本类型宽度多为实现定义,标准只给最小值假定int/long固定宽度

有符号性char的符号性由实现决定拿char当小整数

初始化优先列表初始化,禁止窄化容器花括号与圆括号语义不同

const/constexpr前者只读,后者可编译期求值混用两者语义

auto默认丢弃引用与顶层const忘记写&导致拷贝

宏纯文本替换,无类型无作用域多次求值、缺括号、命名冲突

预定义宏__FILE__、__LINE__、__cplusplus把平台宏当标准



数据类型和变量是写一切代码的地基,宏则是这座地基旁边的一把「没有护栏的电锯」。理解类型的实现定义边界、坚持列表初始化、用constexpr和模板替代函数式宏,这三条能避开入门阶段绝大多数隐蔽错误。宏不会消失,但应当被收缩到条件编译和少数必须依赖预处理器的角落。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询