在学习C++编程的过程中,变量和基本类型是我们迈入编程世界的第一扇大门。 就像我们在数学中需要数字和符号一样,C++中的变量和类型为我们描述和处理数据提供了基础工具。
掌握这些内容,不仅能让你写出正确的程序,更能帮助你理解计算机是如何存储和操作信息的。 在这一节课中,我们将系统学习C++的各种基本类型、变量的定义与初始化、类型转换、作用域和生命周期等核心概念。

在C++中,我们需要不同类型的数据来表示现实世界中的各种信息。比如,我们需要整数来表示年龄、数量,需要小数来表示价格、温度,需要字符来表示文字,需要布尔值来表示真假判断。这就是为什么C++提供了多种基本类型的原因。
C++定义了一组基本类型,包括算术类型和一个名为void的特殊类型。算术类型用来表示字符(char)、整数(int)、布尔值(bool)和浮点数(float、double、long double)。void类型没有关联的值,只能在少数情况下使用,最常见的是作为不返回值的函数的返回类型。
想象一下,如果你要存储一个人的年龄,用整数就够了,因为年龄不可能是小数。但如果你要存储一个人的身高,就需要小数,因为身高可能是1.75米。如果你要存储一个字符,比如字母'A',就不需要数字,只需要一个字符类型。这就是为什么我们需要不同类型的原因——每种类型都有其特定的用途和优势。
算术类型分为两个类别:整型(包括字符(char)和布尔类型(bool))和浮点型(包括单精度浮点数(float)、双精度浮点数(double)和扩展精度浮点数(long double))。
算术类型的大小(即位数)在不同的机器上会有所不同。标准保证的最小大小如下表所示。但是,编译器可以使用更大的大小来表示这些类型。由于位数不同,一个类型能表示的最大(或最小)值也会不同。
C++ 算术类型
bool类型用来表示真值true和false,true和false的值分别为1和0。这是最简单的类型,只用来表示"是"或"否"。
字符类型中,基本的字符类型是char。char保证足够大,能够容纳机器基本字符集中字符对应的数值。也就是说,char的大小与单个机器字节相同。其余的字符类型例如char16_t和char32_t适用于扩展字符集。char16_t和char32_t类型用于Unicode字符,支持中文、日文、阿拉伯文等各种语言的字符。
整型之间的主要区别是能表示的数值范围。short能表示的数最小,long long能表示的数最大。语言保证int至少和short一样大,long至少和int一样大,long long至少和long一样大。long long类型是由新标准引入的,用来处理非常大的整数。
浮点型的区别在于精度。float精度最低但速度最快,double精度较高且最常用,long double精度最高但占用内存最多。在大多数情况下,我们使用double就足够了。
在计算机里,所有数据最终都变成一串0和1。比如: 00011011011100010110010000111011 ...
计算机的内存就像一个很长的格子,每个格子能存一点点数据。最小的格子叫“字节”(byte),通常一个字节有8位(8个0或1)。几个字节合起来,组成一个“字”(word),比如4个字节或8个字节。
每个字节都有自己的编号,这个编号叫“地址”,就像小区里每个房间的门牌号。比如:
左边是地址,右边是这个字节里存的内容。
我们可以用地址找到内存里的任意一块数据。比如,可以说102400这个地址开始的4个字节是一组数据,也可以只看102403这个字节。
但光有0和1还不够,我们还要知道这些0和1代表什么。比如,这4个字节如果当作一个float(小数)来看,就是一个小数;如果当作4个char(字符)来看,可能就是4个字母。类型告诉计算机:这些0和1应该怎么解释。 所以,类型很重要。它决定了数据占多少空间,也决定了怎么把0和1变成我们能理解的数字、字母或其他信息。
下面是一个C++类型小游戏,请选择你认为对的数据类型。
在C++中,很多整数类型(比如int、short、long等)都分为“有符号”(signed)和“无符号”(unsigned)两种。那为什么要这样区分呢?
其实很简单:有些数字在现实中可能是负数,比如温度、银行账户的余额、海拔高度(可以低于海平面),这时候我们就需要“有符号”类型,它能表示正数、负数和零。
但有些数字永远不会是负数,比如学生人数、商品库存、文件的字节数、数组的下标等。这时候我们用“无符号”类型更合适,因为它只表示零和正数。这样不仅更符合实际含义,还能让同样大小的内存表示更大的正整数。
举个例子:
signed char,能表示-128到127。unsigned char,能表示0到255。同样是8位,如果不用来表示负数,全部用来表示正数,范围就扩大了一倍。
在C++里,int默认是有符号的。如果你想用无符号类型,只需要在前面加unsigned,比如unsigned int,甚至可以直接写unsigned。
字符类型稍微特殊一点,C++有三种基本字符类型:char、signed char和unsigned char。虽然名字很像,但它们其实是不同的类型。char到底是有符号还是无符号,要看编译器的实现。
int、signed int)。unsigned int),这样能表示更大的正整数。在C++中,每个变量的类型不仅决定了它能存什么样的数据,还决定了它能做哪些操作。有时候,我们会把一种类型的数据赋值给另一种类型的变量,这时就会发生“类型转换”。
类型转换有时候是自动发生的,比如你把一个小数赋值给一个整数变量,编译器会自动帮你把小数部分去掉,只留下整数部分。有时候你需要手动转换,比如用强制类型转换。
举几个常见的例子:
|int age = 20.8; // age会变成20,小数部分被丢弃 float price = 99; // price会变成99.0,整数自动变成小数 bool flag = -5; // flag会变成true,只要不是0都是真 int yes = true; // yes会变成1,true就是1 unsigned int u = -3; // u会变成一个很大的正数,比如4294967293(32位系统) char ch = 65; // ch会变成字符'A',因为65是A的ASCII码
类型转换的结果,取决于目标类型能表示的范围和规则。比如:
有些类型转换是危险的,尤其是超出类型范围时。比如,给unsigned类型赋负数,或者给signed类型赋超大数,结果就很难预测了。 这样的代码有时候在某些编译器下能跑,在别的环境下就会出错,非常难调试。 对于这种我们无法预测结果的代码,编译器会给出警告,但是作为一个程序员,我们不应该依赖编译器来保证代码的正确性,而是应该自己检查代码,确保代码的正确性。
编译器在需要的时候会自动做类型转换,比如你用一个int变量做条件判断:
|int is_valid = 11; if (is_valid) // 只要is_valid不是0,这里就会被当作true // 这里会被执行
同样地,如果你把bool类型放到算术表达式里,它会自动变成0或1。例如:
|bool flag = true; int num = flag + 1; // num会变成2,因为true被当作1
有时候,unsigned类型和int类型混合运算也会发生自动转换。例如:
|unsigned a = 1; int b = -50; std::cout << a + b << std::endl; // 输出一个很大的数,因为-50会被当作无符号数处理
类型转换是C++里很常见的现象,但也容易出错。写代码时要注意类型的范围和转换规则,避免出现难以发现的bug。
在C++里,像50、3.14、'A'、"hello"这样的值,叫做字面量。所谓字面量,就是你在代码里直接写出来的具体值。 每个字面量都有自己的类型,比如50是整数类型,3.14是浮点数类型,'A'是字符类型,"hello"是字符串类型。 整数的字面量可以用十进制、八进制或十六进制来写。比如,数字50可以写成50(十进制),也可以写成062(八进制,前面加0),还可以写成0x32(十六进制,前面加0x)。 不过,日常编程中我们最常用的还是十进制。浮点数字面量就是带小数点的数字,比如3.14、0.5、-2.0。你也可以用科学计数法,比如1.23e4表示12300。默认情况下,浮点数字面量的类型是double。 字符字面量就是用单引号括起来的单个字符,比如'A'、'b'、'9'。字符串字面量是用双引号括起来的一串字符,比如"Hello, world!"。需要注意的是,字符串字面量在内存里其实会多一个结尾的特殊字符(\0),用来标记字符串的结束。
有时候我们需要在字符串里表示一些特殊的字符,比如换行、制表符、引号等,这些字符不能直接写出来,就要用转义序列。比如:
|std::cout << "Hello\nWorld!"; // \n表示换行,输出会分两行 std::cout << "\tHi!"; // \t表示制表符,输出会有缩进 std::cout << "\"C++\""; // \"表示双引号,输出"C++"
如果你想用十六进制或八进制来表示字符,也可以用\x和\加数字,比如\x41表示大写字母A,\101也是A(八进制)。
有些字面量可以通过加后缀或前缀来指定类型,比如42U表示无符号整数,3.14f表示float类型的小数,L'A'表示宽字符,u8"你好"表示UTF-8字符串。
最后,true和false是bool类型的字面量,nullptr是空指针字面量。
总之,字面量就是你在代码里直接写出来的具体值。理解字面量的类型和写法,有助于你写出更准确、更高效的代码。
尝试点击转换按钮,看看会发生什么。
在C++中,变量就是给一块内存起了个名字,这样我们就可以通过这个名字来存取和操作数据。每个变量都有自己的类型,类型决定了变量能存多大的数据、能做哪些操作。
比如,int age = 10; 这里的age就是一个变量,类型是int,它在内存里占据一定的空间,可以存储一个整数10。
变量的定义通常包括类型和名字,有时还会有初始值。比如:
|int a = 0, b, c = 10;
这行代码定义了三个变量,都是int类型。其中a和c有初始值,b没有。
有些变量类型不是C++内置的,比如std::string,它是一个可以存放任意长度字符串的类型。你可以这样定义:
|std::string name = "Xiaohu";
有时候你会看到“对象”这个词,其实在C++里,变量和对象很多时候是可以互换使用的。对象本质上就是一块有类型的数据区域。
变量的初始化,就是在变量创建时给它一个初始值。比如:
|double price = 37.1, discount = price * 0.9;
这里discount的初始值用到了前面刚定义的price。
C++里初始化和赋值是有区别的。初始化是在变量刚创建时给它赋值,赋值是变量已经存在后再改变它的值。
C++支持多种初始化方式,比如:
|int a = 5; int b{5}; int c(5);
这些写法都能把变量初始化为5。用大括号{}初始化时,如果初始值可能导致数据丢失(比如用小数初始化整数),编译器会报错,帮你避免一些低级错误。
如果你定义变量时没有给初始值,变量的内容是不确定的。比如:
|int x; int y = x + 5; // 这里x的值是未定义的,所以y的值也是不确定的!
如果你直接用x,结果可能是随机的,甚至导致程序出错。只有在函数外面定义的变量,才会自动初始化为0。大多数情况下,建议你总是给变量一个初始值,这样更安全。
有些类型(比如std::string)即使你不写初始值,也会自动变成一个“空字符串”。但有些自定义类型,必须手动初始化,否则编译器会报错。
C++还支持“声明”和“定义”的区别。声明只是告诉编译器有这个变量,但不分配内存;定义才是真正分配内存。比如:
|extern int a; // 声明,不分配内存 int b = 10; // 定义,分配内存并初始化
变量的名字(标识符)可以用字母、数字和下划线组成,但不能以数字开头,区分大小写。比如name和Name是两个不同的变量。变量名最好能表达变量的含义,这样代码更容易读懂。
变量的作用域,就是变量能被访问的范围。一般来说,变量在哪个大括号里定义,就只能在这个大括号里用。比如:
|int main() { int a = 0; for (int i = 1; i <= 10; ++i) { a += i; } // 这里可以用a,但不能用i }
a在整个main函数里都能用,i只能在for循环里用。
作用域还可以嵌套。外层定义的变量,内层可以访问,但内层如果定义了同名变量,就会“遮住”外层的同名变量。比如:
|int x = 1; { int x = 2; // 这里的x和外面的x不是同一个变量,这里定义的x只在花括号里有效 }
尽管上面代码中,内层定义的x和外层定义的x是两个不同的变量,但是你应该尽可能地不去定义同名变量,因为这会让代码变得难以理解。
下面是C++中已经被保留(reserved)的关键字和标识符表,这些名字你不能用作变量名、函数名或类型名,否则编译器会报错。这些关键字是C++语言本身的组成部分,具有特殊含义。
这些关键字是C++标准规定的,不能用作自定义变量名。除此之外,C++标准库(如std命名空间下的cout、cin、string等)也建议不要用作自定义变量名,以免和库冲突。
在C++中,除了前面讲的那些“基本类型”(比如int、double、char等),还有一些“复合类型”,它们是基于其他类型组合出来的。 最常见的复合类型就是引用(reference)和指针(pointer)。这两种类型都能让你“间接”操作别的变量,是C++编程的核心工具, 也是C++的难点。

引用可以理解为“变量的别名”。你可以给一个已经存在的变量起一个新名字,这个新名字和原变量完全等价。例如:
|int a = 1; int &b = a; // b就是a的另一个名字 b = 2; // 实际上是把a改成了20
引用有几个特点:
如果你写int &r = 10;或者int &r = 3.14;,编译器会报错,因为引用必须绑定到一个真实的变量,不能绑定到字面量或类型不匹配的变量。
指针是C++里非常强大但也容易出错的工具。指针本身是一个变量,它存的是另一个变量的“地址”。你可以通过指针间接访问或修改别的变量。比如:
|int a = 1; int *p = &a; // p存的是a的地址 *p = 2; // 通过指针把a改成20
和引用不同,指针是一个真正的变量,有自己的内存空间。你可以让指针指向不同的变量,也可以让它什么都不指向(比如设为nullptr)。
指针的类型要和它指向的变量类型一致,比如int *只能指向int类型的变量,double *只能指向double类型。
指针有几种常见状态:
你可以用*操作符“解引用”指针,访问它指向的变量。比如*p = 5;就是把p指向的变量改成5。
引用一旦绑定就不能再改,指针可以随时指向别的变量。引用没有自己的内存空间,指针有。指针可以是空的(nullptr),引用必须绑定到一个真实变量。
空指针表示指针没有指向任何对象。你可以用nullptr来初始化指针:
|int *p = nullptr;
以前的C++版本用0或者NULL,现在推荐用nullptr。注意,不能把一个int变量直接赋值给指针,即使它的值是0也不行。
你可以让指针指向不同的变量,比如:
|int a = 1, b = 2; int *p = &a; p = &b; // 现在p指向b
如果你写*p = 3;,是把p指向的变量(现在是b)改成3。
指针也可以参与条件判断。只要指针不是nullptr,条件就为真。你还可以比较两个指针是否相等(即是否指向同一个变量)。
void*是一种特殊的指针类型,可以指向任何类型的变量,但你不能直接通过它访问数据,因为它不知道指向的是什么类型。void*常用于底层操作,比如内存管理。
理解复杂类型声明时,最重要的是搞清楚每个符号到底修饰的是哪个变量。比如你看到一行代码里有多个变量,有的前面有*,有的没有,这些*其实只对它后面的变量有效。
举个例子:
|float* tempPtr, tempValue;
这里tempPtr是一个指向float的指针,而tempValue只是一个普通的float变量。很多初学者会以为两个都是指针,其实不是。
如果你想让多个变量都是指针,应该这样写:
|float *p1, *p2;
这样p1和p2都是指向float的指针。
再比如,声明引用时:
|char letter = 'A'; char &refLetter = letter;
refLetter就是letter的别名。
你还可以有多级指针,比如“指向指针的指针”:
|std::string name = "Tom"; std::string *pName = &name; std::string **ppName = &pName;
ppName是一个指向pName的指针,而pName又指向name。你可以通过**ppName访问到name。
如果你想让一个引用绑定到一个指针变量上,也可以:
|int score = 100; int *scorePtr = &score; int *&refPtr = scorePtr; *refPtr = 80; // 这会把score改成80
这里refPtr其实就是scorePtr的另一个名字。
遇到复杂声明时,建议你从变量名往右读,先看最近的符号。比如double **&x = ...;,&离x最近,所以x是引用,*在&外面,所以x是“指向指针的指针”的引用。
有时候我们希望某个变量的值一旦设定后就不能再被修改。比如你想用一个变量来表示最大登录次数,这样以后如果要改只需要改一个地方,但又不希望程序的其他地方不小心把它改掉。这时就可以用const来修饰变量:
|const int maxLogin = 5; maxLogin = 10; // !!!编译器会报错
这样定义后,maxLogin的值就不能再被修改了。如果你再写maxLogin = 10;,编译器会报错。注意,const变量必须在定义时初始化,否则也会报错。
const变量和普通变量在大多数操作上没区别,比如可以参与运算、可以赋值给别的变量,只是不能被修改。比如:
|int tickets = 3; const int limit = tickets; int left = limit;
const变量的“只读”属性只影响它自己,不影响用它初始化出来的新变量。
如果const变量的初始值是常量(比如上面的5),编译器通常会在编译时直接把它替换成具体的值。这样做的好处是效率高,但如果你的程序分成了多个文件,每个文件都用到了这个const变量,实际上每个文件里都会有一份自己的副本。
如果你希望多个文件共享同一个const变量(比如它的初始值不是常量,而是运行时计算出来的),就要用extern关键字:
|// config.cpp extern const int maxScore = getMaxScore(); // config.h extern const int maxScore;
这样,只有一个maxScore变量,所有文件都用同一个。
const引用是C++中非常强大的特性,它可以让你的代码更安全、更易维护。合理使用const引用,可以防止很多低级错误。
你可以让一个引用指向const变量,这样通过这个引用也不能修改原变量:
|const double pi = 3.1416; const double &refPi = pi;
如果你试图通过refPi去修改pi,编译器会报错。
有意思的是,const引用还可以绑定到普通变量、字面量,甚至是表达式的结果:
|float temp = 36.5f; const float &r1 = temp; // 可以 const float &r2 = 100.0f; // 也可以 const float &r3 = temp + 1; // 也可以
如果你写float &r4 = temp + 1;,就会报错,因为普通引用不能绑定到临时值或字面量。
const引用的本质是“只读视图”,它不保证原变量本身就是const,只是通过这个引用不能改而已。原变量本身还是可以被其他方式修改的。
const指针是C++中非常强大的特性,它可以让你的代码更安全、更易维护。合理使用const指针,可以防止很多低级错误。
指针也可以和const结合。你可以有“指向const的指针”,也可以有“自身是const的指针”,还可以两者都const。
|const char letter = 'Z'; const char *ptr = &letter; // 不能通过*ptr改letter
|int counter = 7; int *const p = &counter; // p不能再指向别的变量,但可以*p = 0;
|const char *const pstr = &letter;
理解这种声明的诀窍还是“从变量名往右读”,先看最近的const和*修饰的是什么。
有时候,C++里的类型写起来很长、很复杂,比如你要用到某种指针、某种模板类型,写一遍很容易出错。这时你可以给类型起个“别名”,让代码更简洁、更易懂。
最传统的做法是用typedef,比如:
|typedef unsigned long long BigNumber; BigNumber a = 1234567890;
这样以后你就可以用BigNumber来代替unsigned long long。
C++11之后,还可以用using来起别名,写法更直观:
|using Text = std::string; Text name = "Alice";
类型别名不仅能让代码更短,还能让你的意图更清晰,比如你可以用using给某种特殊用途的指针起个名字,让别人一看就知道这个变量是干什么的。
需要注意的是,如果你用类型别名给指针起别名,比如:
|typedef int* IntPtr; const IntPtr p = nullptr;
这里p其实是“常量指针”,而不是“指向常量的指针”。也就是说,你不能让p再指向别的地方,但可以通过p修改它指向的内容。这和const int*是完全不同的意思。
有时候你写代码时,变量的类型很复杂,或者你根本不关心它是什么类型,只想让编译器自动帮你推断。这时就可以用auto:
|auto total = 3.14 * 2; // total会自动变成double类型
只要你给变量一个初始值,编译器就能自动推断出类型。比如你写:
|auto flag = true; // flag是bool类型 auto ptr = &total; // ptr是double*类型
如果你在一行里用auto定义多个变量,所有变量的类型必须能兼容,比如:
|auto x = 10, y = 20; // 没问题,都是int auto a = 1, b = 2.5; // 报错,类型不一致
auto在遇到const和引用时有一些细节。比如:
|const int max = 100; auto value = max; // value是int,不带const const auto another = max; // another是const int auto &ref = max; // ref是const int&,因为max是const
如果你想让auto推断出来的变量带上const或者是引用,需要自己加上const或&。
有时候你想让变量的类型和某个表达式一致,但又不想用这个表达式初始化变量,只想让编译器帮你“照抄”类型。这时可以用decltype:
|double getPrice(); decltype(getPrice()) price; // price的类型和getPrice()的返回值一样
decltype和auto有点像,但也有区别。比如,decltype会保留变量的const和引用属性:
|const float pi = 3.14f; decltype(pi) a = 0; // a是const float float b = 2.0f; float &rb = b; decltype(rb) c = b; // c是float&,必须初始化
如果你用decltype分析一个表达式,比如解引用指针:
|int num = 7; int *pNum = # decltype(*pNum) refNum = num; // refNum是int&,必须初始化
还有一个容易混淆的地方:如果你在decltype里加了括号,比如decltype((num)),结果一定是引用类型;而decltype(num)只有在num本身是引用时才是引用类型。
现在让我们通过一些简单的练习题来巩固本章学到的知识。每道题都涵盖了类型、变量和类型转换的核心概念,请先尝试独立完成,然后再查看答案。
你想编写一个程序来存储学生的信息,包括年龄(整数)、身高(小数,精确到小数点后两位)、是否及格(是或否)。请写出定义这三个变量的代码。
|#include <iostream> using namespace std; int main() { int age = 18; // 年龄用 int 类型存储 float height = 175.5; // 身高用 float 类型存储(也可以用 double) bool isPassed = true; // 是否及格用 bool 类型存储 cout << "年龄:" << age << endl; cout << "身高:" <<
请写一行代码,同时定义三个 int 类型的变量 a、b、c,其中 a 初始化为 10,c 初始化为 30,b 不初始化。
|int a = 10, b, c = 30;
在 C++ 中,可以在同一行定义多个同类型的变量,用逗号分隔。每个变量可以单独初始化,也可以不初始化。注意,未初始化的变量(如 b)的值是不确定的,使用前应该先赋值。
下面的代码会输出什么?请解释每一步发生了什么。
|#include <iostream> using namespace std; int main() { float f = 3.7; int i = f; cout << i << endl; return 0; }
输出结果是 3。
当我们将一个 float 类型的小数赋值给 int 类型的变量时,会发生隐式类型转换。小数部分会被直接截断(不是四舍五入),所以 3.7 变成了 3。这种转换会丢失精度,是向下取整的过程。
下面的代码会输出什么?为什么?
|#include <iostream> using namespace std; int main() { unsigned int u = -1; cout << u << endl; return 0; }
输出结果是一个很大的正数(通常是 4294967295)。
unsigned int 是无符号整数类型,只能存储非负数。当我们给一个无符号类型赋一个负值时,会发生类型转换。负数在内存中是以补码形式存储的,当被解释为无符号数时,就会变成一个很大的正数。这是因为无符号类型把最高位也当作数值位,而不是符号位。
编写一个程序,读取用户输入的一个小数,然后将其转换为整数并输出。同时输出转换前后的值,让用户看到转换的效果。
|#include <iostream> using namespace std; int main() { float original; cout << "请输入一个小数:"; cin >> original; int converted = original; // 隐式类型转换,小数部分会被截断 cout << "原始值:" << original << endl; cout << "转换后的值:" << converted <<
这道题考察的是如何根据数据的特性选择合适的类型。年龄是整数,所以用 int;身高需要小数,用 float 或 double;是否及格是二选一的情况,用 bool 最合适。
这个程序展示了类型转换的实际应用。当用户输入 3.7 时,程序会输出原始值 3.7 和转换后的值 3,让用户清楚地看到类型转换的效果。注意,这种转换是向下取整,不是四舍五入。