C++ primer 第二章 变量和基本类型
2.1 基本内置类型
算数类型
C++的基本数据类型包括算术类型和空类型。
算数类型包括字符、整形数、布尔值和浮点数。
常见的类型和所占位数为:
类型 | 最小尺寸 |
---|---|
bool | 未定义 |
char | 8位 |
short | 16位 |
int | 16位 |
long | 32位 |
long long | 64位 |
float | 6位有效数字,32位 |
double | 10位有效数字,64位 |
long double | 10位有效数字 |
C++语言规定一个int至少和一个short一样大,一个long至少和一个int一样大。(int也可能32位)
整形还可以分为带符号的(signed)和无符号的(unsigned)
int、short、long和long long都是带符号的
字符型被分为三种:char、signed char和unsigned char。
字符型被分为三种,但是表现形式只有两种,char可能有符号,也可能无符号,编译器决定。
类型转换
不要混用带符号数和无符号数
字面值常量
可以将整形字面值写成十进制、八进制、十六进制。
八进制0开头、十六进制0X开头。
负数并不是字面值常量,负号并不在字面值之内,仅仅是对字面值取负数而已。
单引号括起来的一个字符为char型字面值,双引号括起来的零个或多个字符构成字符串型字面值。
true和false是布尔类型的字面值。
nullptr是指针类型字面值。
有些符号不能直接打印,需要用到转义序列。
符号 | 命令 |
---|---|
换行符 | \n |
横向制表符 | \t |
退格符 | \b |
单引号 | \' |
反斜线 | \\ |
问号 | ? |
2.2 变量
变量定义
变量定义的基本形式是:类型说明符,随后紧跟一个或多个变量名组成的列表,同时还可以为变量赋初值。
例如: int sum = 0, value;
当对象在创建时获得一个特定的值,我们说这个对象被初始化了
列表初始化:用花括号来初始化变量
int a = {0};
int b{0};
*当用于内置类型时,如果使用列表初始化且初始值存在丢失风险,编译器将报错。
例如:
long double ld = 3.1415;
int a{ld}, b = {ld}; #错误,存在丢失风险
int c(ld), d= ld; #正确,且丢失了部分值
变量声明和定义关系
一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。
声明使得名字为程序所知,定义负责创建与名字关联的实体。
变量的声明和定义都规定了类型和名字,但是定义还申请存储空间,也可能为变量赋初值。
如果想声明一个变量而非定义,在变量名前加extern,而且不要显示地初始化变量。
例如:
extern int i; #声明i而非定义i
int j; #声明并定义j
包含显示初始化的声明即称为定义。
extern double pi = 3.14; #定义
标识符
由字母、数字、下划线构成,并且第一个不能是数字。
标识符不能是关键字
变量名一般小写,如index
类名一般首字母大写
多个单词,遵循驼峰原则
作用域
局部作用域和全局作用域
2.3 复合类型
引用
引用就是为对象起了一个别名,声明符写成&d来定义引用类型,d为变量名
引用必须初始化
int ival = 1024;
int &refval = ival;
int &refval2; #报错:引用必须初始化
一般初始化时,初始值拷贝到新建对象中,引用则是和初始值绑定在一起
引用不是对象,只是一个别名
引用必须严格匹配,并且不能绑定字面值。
int &i = 1024; #错误,绑定字面值
double dval = 3.14;
int &refval = dval; #错误,引用类型初始值必须是int类型
指针
指针是指向另外一种类型的复合类型,指针本身就是一个对象,而且无需定义时赋值。
定义指针的方法写成*d,其中d是变量名。
如果一条语句中定义了几个指针变量,每个变量前都要有符号*。
指针存放对象的地址,想要获取地址,需要使用取地址符(操作符&)
int *p1, p2, *p3;
int ival =42;
int *p = &ival;
指针类型要和指向的类型严格匹配
double dval;
double *pd = &dval;
int *pi = pd; #错误,类型不匹配
如果指针指向一个对象,则允许使用解引用符(操作符*)来访问该对象。
对指针解引用会得出所指的对象。
int ival = 42;
int *p = &ival;
cout << *p; #输出42
空指针不指向任何对象,试图使用一个指针之前可以检查他是否为空。
得到空指针最直接的方法就是用字面值nullptr
int *p1 = nullptr;
*建议初始化所有指针,使用未初始化的指针,将出现重大错误。
void *指针
可以存放任意对象的地址,但是不知道该地址中是什么类型对象。
所以不能直接操作void *指针类型对象,只能和其他指针比较,作为函数输入输出等。
复合类型声明
变量的定义包括一个基本数据类型和一组声明符,在同一条定义语句中,基本数据类型只有一个,声明符形式却可以不同。
int i = 1024, *p = &i, &r = i;
i是一个int型的数,p是一个int型指针,r是一个int型引用
int* p1, p2; //p1是指向int的指针,p2是int
指向指针的指针
通过*区分指针的级别,**表示指向指针的指针。
int ival = 1024;
int *pi = &ival; //pi指向一个int型的数
int **ppi = &pi ; //ppi指向一个int型指针
指向指针的引用
引用本身不是对象,所以不存在指向引用的指针,但指针是对象,所以存在指向指针的引用。
int i = 42;
int *p;
int *&r=p; //r是对指针p的引用
r = &i; //r引用指针,所以是p指向i
*r = 0; //解引用得到i,也就是p指向对象,i变成0
要理解r的类型是什么,最简单的方法就是从右往左读,最先遇到&符号,所以是一个引用,然后遇到*,说明引用了一个指针,然后遇到int,指出引用的是一个int型指针。
2.4 const限定符
const可以定义常量,任何试图对常量赋值的行为都会报错。
const int bufSize = 512;
bufSize = 256; //错误,试图向const对象写值
初始化和const
如果利用一个对象去初始化另一个对象,它们是不是const都无关紧要。
默认状态下,const对象仅在文件内有效
编译器在编译过程中,会把所有常量替换成对应的值,所以const对象仅在文件内有效
如果想在其他文件内也有效,需要加上extern关键字
file_1.cc
extern const int bufSize = 256;
file_1.h
extern const int bufSize; //与file_1.cc中定义的bufSize是同一个
const的引用
把引用绑定在const对象上,称为对常量的引用。
const int ci = 1024;
const int &r1 = ci; //正确
r1 = 42; //错误,不能对常量赋值
int &r2 = ci; //错误,非常量引用绑定常量对象
可以将常量引用绑定到变量上
int i = 42;
const int &r1 = i; //正确
不可以将变量引用绑定到常量上
int &r4 = r1; //错误,r4是一个普通的非常量引用
一般引用的类型与被引用的需要一致,但是初始化常量引用允许是任意表达式,只要能转换成引用的类型即可。
double dval = 3.14;
const int &ri = dval; //正确
这里ri实际上绑定了一个临时量对象,相当于:
const int temp = dval;
const int &ri = temp;
const引用绑定一个变量时,不能通过const对象改变值,但是通过其他引用改值允许。
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; //正确
r2 = 0; //错误,常量引用不能赋值
指针和const
指向常量的指针不能用于改变其所属对象的值。要想存放常量对象的地址,只能使用常量指针。写法为:const int *ptr
const double pi = 3.14;
double *ptr = & pi; //错误,不是指向常量的指针
const double *cptr = & pi; //正确,指向常量的指针
cptr = 42; //错误,不能赋值
指针的类型必须和所指向的类型一致,但是也可以指向非常量
double dval = 3.14;
cptr = &dval; //正确
指向常量的指针和对常量的引用一样,都可以指向或者绑定变量,只是不能通过它们去改变值而已,但是可以通过其他指向或者绑定的变量去改变值。
指向常量的指针不能改变常量,但是可以指向其他常量。
const指针
指针本身为常量,称为常量指针。常量指针必须初始化,而且一旦初始化完成,它的值就再也不能改变。写法为:int *const ptr
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14;
const double *const pip = & pi; //pip是一个指向常量的常量指针
常量指针指向的对象不能改变,但是可以改变指向对象的值。
*curErr = 0;
顶层const
顶层const表示指针本身是个常量。
底层const表示指针所指的对象是个常量。
更一般的,顶层const可以表示任意对象本身是常量,底层const表示被指向的是常量。
int i = 0;
int *const p1 = &i; //顶层const
const int ci = 42; //顶层const
const int *p2 = &ci; //底层const
const int *const p3 = p2; //左侧底层const,右侧顶层const
cosnt int &r = ci; //引用都是底层const
当拷贝时,顶层const不受干扰,但是底层const必须具有相同的底层const资格
int *p = p3; //错误,p3有底层const,p没有
p2 = p3; //正确,p2,p3都有底层const,顶层const可以忽略
p2 = &i; //正确,int*能转换成const int *
int &r = ci; //错误,普通引用不能绑定常量上
const int &r2 = i; //正确,const 引用可以绑定到普通变量上
constexpt和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象是不是常量表达式由数据类型和初始值共同决定。
const int max_files = 20; //max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式
const itn sz = get_size(); //sz不是常量表达式
constexpr变量
c++11新标准规定,允许将变量声明为constexpr类型,以便编译器验证是否是常量表达式。声明为constexpr的变量一定是常量,而且必须用常量表达式初始化。
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; //mf + 1是常量表达式
constexpr int sz = size(); //只有当size函数是constexpr函数,才是正确声明语句
字面值类型
算数类型、引用、指针都是字面值类型。
自定义类、IO库、String类型都不是字面值类型。
指针和constexpr
在constexpr声明中定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。
const int *p = nullptr; //p是指向整型常量的指针
constexpr int *q = nullptr; //q是指向整形的常量指针
constexpr把他定义的对象置为顶层const。
constexpr指针既可以指向常量,也可以指向非常量。
2.5 处理类型
类型别名
类型别名是给类型起一个新的名字。有两种方法可以定义类型别名。
传统方法使用typedef
typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double* 的同义词
C++11规定了一种新方法,使用别名声明来定义
using SI = double; //SI是double的同义词
wages hourly,weekly; //等价于doubel hourly,weekly;
SI hourly,weekly; //等价于doubel hourly,weekly;
指针、常量和类型别名
如果类型别名指代的是复合类型或常量,不能进行替换理解。
typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,他的对象是指向char的常量指针
pstring实际上是指向char的指针,因此,const pstring是指向char的常量指针,而非指向常量字符的指针。
不要错误的尝试把类型别名替换成它本来的样子,
const char *cstr = 0; //对const pstring cstr的错误理解
auto类型说明符
auto类型让编译器替我们去分析表达式所属的类型
auto item = val1 + val2; //item类型由val1和val2决定
auto定义的变量,必须有初始值。
auto也可以在一条语句声明多个变量。因为一条语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一样。
auto i = 0, *p = &i; //正确,i整形,p是整形指针
auto sz = 0, pi = 3.14; //错误,sz和pi的类型不一致
复合类型、常量和auto
auto和引用一起时,实际上auto以引用所绑定的对象的类型为auto类型。
int i = 0, &r = i;
auto a = r; //a是int类型
auto一般会忽略顶层const,保留底层const
const int ci = i , &cr = ci;
auto b = ci; //b是整数
auto c = cr; //c是整数
auto d = &i; //d是整形指针
auto e = &ci; //e是指向整数常量的指针
如果需要auto类型是一个顶层const,需要明确指出
const auto f = ci; //ci推演是int,f是const int
还可以将引用设置为auto
auto &g = ci; //g是一个整形常量引用,绑定在ci上
auto &h = 42; //错误,不能为非常量引用绑定字面值
const auto &j = 42; //正确,可以为常量引用绑定字面值
符号&和*只属于某个声明符,而不属于基本数据类型
auto k = ci , &l = i; //int
auto &m = ci, *p = &ci; //const int
auto &n = i ,*p2 = &ci; //错误
decltype类型提示符
decltype作用是返回操作数的类型,分析表达式的类型,确不计算他的值
decltype(f()) sum = x; //sum的类型就是f的返回值类型
decltype处理顶层const和引用的方式和auto不同
如果decltype使用的表达式是一个变量,则返回该变量的类型(包括顶层const和引用)
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //x的类型是const int
decltype(cj) y = x; //y的类型是const int&
decltype(cj) z; //错误,z是引用,必须初始化
decltype和引用
如果decltype使用的表达式不是变量,则返回表达式结果对应的类型。
int i = 42, *p = &i, &r = i;
decltype(r+0) b; //b是int类型
decltype(*p) c; //错误,c是int &类型,没有初始化
r是一个引用,decltype(r)是引用,decltype(r+0)是非引用
如果表达式内容是解引用操作,则decltype将得到引用类型,因此,decltype(*p)结果是int &,而非int。
decltype如果给变量加括号就是引用,不加括号就是变量
decltype((i)) d; //错误,d是int & ,必须初始化
decltype(i) e; //正确,e是int类型
2.6 自定义数据结构
结构体(类)
以关键字struct开始,紧跟着类型和类体(类体可以为空),类体由花括号包围形成一个新的作用域,类内部定义名字必须唯一,但是可以与类外部定义名字重复。
struct Sales_data {
std::string bookNo;
double revenue = 0.0;
}
类体花括号后面必须紧跟一个分号,因为类体后面可以紧跟变量名以示对类型对象的定义。
struct Sales_data{} accum,trans,*salesptr;
等价于
struct Sales_data{};
Sales_data accum, trans, *salesptr;
建议不要把类的定义和变量定义放一起写
类数据成员
类体内只有数据成员,类的数据成员定义了类对象的具体内容,每个对象有自己的一份数据拷贝,修改对象的数据成员,不会影响其他Sales_data的对象。
定义类内成员和普通变量相同,一个基本数据类型,一个或多个声明符。
C++11新标准规定,可以为数据成员提供一个类内初始值,创建对象时,类内初始值用于初始化数据成员。
使用结构体(类)
引入结构体文件
#include "Sales_data.h"
定义结构体变量
Sales_data data1, data2;
调用结构体变量,使用“点”完成调用
std:cin>>data1.bookNo>>data1.units_sold;
编写自己头文件
类通常定义在头文件中,并且类的名字和头文件名一致
如Sales_data定义在Sales_data.h头文件中
头文件通常包含只能定义一次的实体,包括类、const、constexpr变量。
头文件还可以包含其他头文件,例如Sales_data用到String,所以包含String.h头文件
预处理器
确保头文件多次包含仍能安全工作的常用技术是预处理器
例如预处理功能#include,当预处理器看到#include标记时,就会用指定的头文件内容代替#include。
C++程序还会用到一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。
预处理变量有两种状态,已定义和未定义。
#define指令把一个名字设定为预处理变量
#ifdef检测预处理变量是否定义,当且仅当已定义为真
#ifndef检测预处理变量是否定义,当且仅当未定义为真
一旦检测结果为真,则执行后续操作,直至遇到#endif指令为止
有些头文件会多次包含(嵌套方式),使用上述功能可以有效防止重复包含的发生,正常工作。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
第一次遇到sales_data.h时,#ifndef检测结果为真,预处理器顺序执行后面操作,直到遇到#endif为止。此时预处理变量SALES_DATA_H的值变为已定义,而SAles_data.h也会拷贝到程序中来,如果再一次包含Sales_data.h,则#ifndef将变成假,编译器将忽略#ifndef和#endif之间的部分。
整个程序中,预处理变量包括头文件保护符必须唯一,一般用类的名字构建保护符名字,为了避免与其他实体发生冲突,一般把预处理变量的名字全部大写。
头文件的保护符一般建议加上,即使现在还没有被别的程序包含。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律