基本内置类型#
不同的编译器和平台可能会有差异,下表为在 Windows_X64 环境下使用 MinGw GCC 9.2.0 32_bit 为例:
类型 | 含义 | 最小尺寸 | 范围 |
---|---|---|---|
bool | 布尔类型 | 8bits | true 或 false |
char | 字符型 | 8bits | -2^7 ~ 2^7-1 |
wchar_t | 宽字符 | 16bits | 0 ~ 2^16 - 1 |
char16_t | Unicode 字符 | 16bits | 0 ~ 2^16 - 1 |
char32_t | Unicode 字符 | 32bits | 0 ~ 2^32 - 1 |
short | 短整型 | 16bits | -2^15 ~ 2^15-1 |
int | 整型 | 32bits | -2^31 ~ 2^31-1 |
long | 长整型 | 32bits | -2^31 ~ 2^31-1 |
long long | 长整型 | 64bits | -2^63 ~ 2^63-1 |
float | 单精度浮点型 | 32bits | 大约 7 位有效数字 |
double | 双精度浮点型 | 64bits | 大约 15 位有效数字 |
long double | 扩展精度浮点型 | 96bits | 更高的有效数字 |
可以在 IDE 中编译运行以下代码查看本机算数类型的尺寸
// Windows_X64环境下MinGw GCC 9.2.0 32_bit为例
// 1Byte = 8bit
#include<iostream>
using namespace std;
int main(){
cout<< "int * "<<sizeof(int*)*8<<endl; // 32 bits
cout<< "bool "<<sizeof(bool)*8<<endl; // 8 bits
cout<< "char "<<sizeof(char)*8<<endl; // 8bits
cout<< "wchar_t "<<sizeof(wchar_t)*8<<endl; // 16bits
cout<< "char16_t "<<sizeof(char16_t )*8<<endl; // 16bits
cout<< "char32_t "<<sizeof(char32_t )*8<<endl; // 32bits
cout<< "short "<<sizeof(short )*8<<endl; // 16bits
cout<< "int "<<sizeof(int )*8<<endl; // 32bits
cout<< "long "<<sizeof(long )*8<<endl; // 32bits
cout<< "long long"<<sizeof(long long)*8<<endl; // 64bits
cout<< "float "<<sizeof(float)*8<<endl; // 32bits
cout<< "double "<<sizeof(double)*8<<endl; // 64bits
cout<< "long double "<<sizeof(long double)*8<<endl; //96bits
return 0;
}
内置类型的机器实现#
字节 (Byte) 是可寻址的最小内存块,1 byte = 8 bit (比特)
字 (Word) 是存储的基本单元,1 Word = 4 字节或 8 字节
内存中每个字节与⼀个数字(被称为地址 address)关联,我们能够使用某个地址来表示从这个地址开始的大小不同的比特串,如地址为 736424 的那个字或者地址 736424 的那个字节。但是必须知道存储在某地址的数据类型,才能赋予内存该地址明确含义,类型决定数据所占比特数,以及如何解释这些比特的内容。
带符号型和无符号型#
其它整型
-
除去布尔型和扩展字符型外,其它整型可划分为带符号的和无符号的
-
int, short, long, long long 带符号
-
类型名前添加
unsigned
得到无符号类型 -
如
unsigned long
-
unsigned int 可简写为 unsigned)
字符型
-
分三种 char, signed char, unsigned char
-
char 表现为带符号还是无符号的,由编译器决定
如何选择类型#
- 数值不可能是负数时,比如年龄,长度等等,选用
无符号类型
; - 整数计算式使用
int
类型,一般long
的大小和int
一样,而short
显得太小。如果处理较大的整数,应该选择long long
; - 算数表达式中不要使用
bool
或char
,符号容易出问题; - 浮点数据运算使用
double
;
类型转换#
类型所能表示值的范围决定转换过程:
- 非布尔型赋给布尔型,初始值为 0 则结果为 false, 否则为 true。
#include<iostream>
using namespace std;
int main() {
int i = 42;
if (i) {
i = 0;
}
cout << "i = " << i << endl; // i=0
return 0;
}
- 布尔型赋给非布尔型,初始值为 false 结果为 0,初始值为 true 结果为 1。
- 浮点数赋给整型,近似处理,保留小数点前的部分。
- 整型赋给浮点型,小数部分记 0,若整型超过浮点型容量则精度可能有损失。
- 超出范围值赋给无符号数,结果是初始值对无符号类型表示的最大值取模余数。
- 超出范围值赋给有符号数,结果未定义。
//test01.cpp
#include<iostream>
using namespace std;
int main() {
int i = 42;
if (i) {
i = 0;
}
cout << "i = " << i << endl; // i=0
return 0;
}
//test02.cpp
#include<iostream>
int main() {
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; //-84
std::cout << u + i << std::endl; //4294967264
return 0;
}
//test03.cpp
#include<iostream>
int main() {
for (int i = 10; i >= 0; --i) {
std::cout << i << std::endl;
}
return 0;
} //正常输出
//test04.cpp
#include<iostream>
int main() {
for (unsigned u = 10; u >= 0; --u) { //无符号数不会小于0
std::cout << u << std::endl; //死循环,ctrl+c退出
}
return 0;
}
//test05.cpp
#include<iostream>
int main() {
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl; //32
std::cout << u - u2 << std::endl; //4294967264
int i = 10, i2 = 42;
std::cout << i2 - i << std::endl; //32
std::cout << i - i2 << std::endl; //-32
std::cout << i - u << std::endl; //0
std::cout << u - i << std::endl; //0
return 0;
}
字面值常量 (Literal Constants)#
- 整型字面值常量:
- 十进制 :42
- 八进制: 052 (以 0 开头)
- 十六进制: 0x2A 或 0X2A (以 0x 或 0X 开头)
- 二进制:
0b101010
或0B101010
(以 0b 或 0B 开头)
错误示范:
#include<iostream>
int main() {
int month = 9, day = 7;
int month1 = 09, day1 = 07; //以0开头会当成8进制赋值
// [Error] invalid digit "9" in octal constant
return 0;
}
- 浮点型字面值常量:
- 十进制:
3.14
或2.0
- 科学计数法:
3.0e8
(表示 3.0 乘以 10 的 8 次方)
- 字符型字面值常量:
- 字符:
'A'
或'x'
- 转义字符:
'\n'
(表示换行符)或'\\'
(表示反斜杠)
转义序列:
换行'\n' 横向制表符'\t' 响铃'\a'
纵向制表符'\v' 退格符'\b' 双引号'\"'
反斜线'\\' 问号'\?' 单引号'\'' 回车'\r'
泛化转义序列:'\x'跟1个多个16进制数,或'\'后跟1、2、3个8进制数
'\7'响铃 '\12'换⾏ '\40'空格
'\0'空字符 '\115'字符M '\x4d'字符M
//test05.cpp
#include<iostream>
int main() {
std::cout << '\n'; //换行
std::cout << "\tHi!\n"; //制表符,Hi!,换行
std::cout << "\v\?\abc\b\n"; //纵向制表符,?,响铃,bc,退格,换行
std::cout << "Hi \x4dO\115!\n"; //Hi MOM!,换行
std::cout << '\115' << '\n'; //M,换行
return 0;
}
- 字符串字面值常量:字符串字面值常量表示一串字符,使用双引号括起来。例如:
- 字符串:
"Hello, World!"
字符串实际类型是字符数组,结尾添加 '\0' 字符 字符串型实际上是常量字符构成的数组, 结尾处以 '\0' 结束,所以字符串类型实际 上长度比内容多 1。
-
布尔字面值常量:布尔字面值常量表示真或假,可以是
true
或false
。 -
指针字面值: nullptr
-
指定字面值类型:
字符和字符串字面值:
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16 字符 | char16_t |
U | Unicode32 字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8 (字符串字⾯常量) | char |
整型字面值:
后缀 | 最小匹配类型 |
---|---|
u or U | unsigned |
l or L | long |
ll or LL | long long |
浮点型字面值:
后缀 | 类型 |
---|---|
f or F | float |
l or L | long double |
变量#
变量提供⼀个具名的、可供程序操作的存储空间。 C++ 中变量和对象⼀般可以互换使用
define#
-
定义形式:类型说明符(type specifier) + ⼀个或多个变量名组成的列表;
-
初始化(initialize):对象在创建时获得了⼀个特定的值。
-
- 初始化不是赋值 ,初始化 = 创建变量 + 赋予初始值 ,赋值 = 擦除对象的当前值 + 用新值代替
-
列表初始化:使用花括号 {}
-
若列表初始化且初始值存在丢失信息的风险,则编译器将报错。
int a{3.14}; // error: narrowing conversion from ‘double’ to ‘int’
-
默认初始化:定义时没有指定初始值会被默认初始化;
-
在函数体内部的内置类型变量将不会被初始化。
-
类的对象如果没有显⽰初始化,其值由类定义。
#include<iostream>
std::string global_str; //定义全局变量global_str,初值为空字符串
int global_int; //定义全局变量global_int,初值为0
int main() {
int local_int; //局部变量未初始化,初值未定义
std::string local_str;
//local_str 是 string类的对象,它的值由类确定,为空字符串。
return 0;
}
建议初始化每⼀个内置类型变量!
变量声明与定义的联系#
把程序拆分成多个逻辑部分来编写 ,C++ 支持分离式编译机制,程序分割为若干文件,每个文件可被独立编译,为了支持分离式编译,C++ 将声明和定义区分开。
声明使得名字为程序所知,定义负责创建与名字关联的实体。
声明规定了变量类型和名字。定义申请存储空间,也可能为变量赋初值。想声明⼀个变量而非定义它,在变量名前加关键字extern
, 不要显示初始化变量。
变量只能被定义⼀次,但是可以多次声明,定义只出现在⼀个文件中,其他文件使用该变量时需要对其声明。
extern int i; //声明i而非定义
int j; //声明并定义j
// 包含了显示初始化的声明,就变成了定义:
extern double pi = 3.14; //定义
// 函数体内部,试图初始化⼀个extern关键字标记的变量,将引发错误。
extern int ix = 1024; //定义
int iy; //定义
extern int iz; //声明
变量标识符与命名规范#
标识符:
- 由字母、数字、下划线组成,必须以字母或下划线开头。
-
长度没有限制。
-
大小写写敏感。
-
保留名字不能用做标识符。
-
用户自定义标识符
- 不能连续出现两个下划线
- 不能以下划线紧跟大写字母开头
- 定义在函数体外的标识符不能以下划线开头
- 使用小驼峰命名法
//c++关键字
alignas continue friend register true
alignof decltype goto reinterpret_cast try
asm default if return typedef
auto delete inline short typeid
bool do int signed typename
break double long sizeof union
case dynamic_cast mutable static unsigned
catch else namespace static_assert using
char enum new static_cast virtual
char16_t explicit noexcept struct void
char16_t export nullptr switch volatile
class extern operator template wchar_t
const false private this while
constexpr float protected thread_local
const_cast for public throw
//c++操作符替代名
and bitand compl not_eq or_eq xor_eq
and_eq bitor not or xor
命名规范:
- 体现实际含义
- 变量名和函数名使用小驼峰命名法,如
myVariable
- 自定义类名大写字母开头,如
Sales_item
- 若标识符有多个单词组成,单词之间要有明显区分,如
student_loan
,studentLoan
, 不要用studentloan
int double = 3.14; //error
int _;
int catch-22; //error
int 1_or_2 = 1; //error
double Double = 3.14;
名字的作用域(namescope),以 {} 分隔
- 全局作用域
- 块作用域
第⼀次使用变量时再定义它, 更容易找到,赋予比较合理的初始值。
//scope.cpp
#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same
int reused = 42; // reused has global scope
int main() {
int unique = 0; // unique has block scope
// output #1: uses global reused; prints 42 0
std::cout << reused << " " << unique << std::endl;
int reused = 0; // new, local object named reused hides global reused
// output #2: uses local reused; prints 0 0
std::cout << reused << " " << unique << std::endl;
//此时全局变量被同名的局部变量覆盖
// output #3: explicitly requests the global reused; prints 42 0
std::cout << ::reused << " " << unique << std::endl;
//使用全局变量
return 0;
}
嵌套的作用域:
同时存在全局和局部变量时,已定义局部变量的作用域中可用::
显式访问全局变量。用到全局变量时,尽量不使用重名的局部变量。
#include <iostream>
int i = 42;
int main() {
int i = 100;
int j = i;
//j的值是100,局部变量i覆盖了全局变量i
std::cout << j << std::endl; //100
return 0;
}
#include <iostream>
int main() {
//下面的程序合法吗?如果合法,它将输出什么?
int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
sum += i; //此时的变量i仅在for循环内生效
std::cout << i << " " << sum << std::endl;
//合法输出:100 45
return 0;
}
左值和右值#
-
左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
-
右值(r-value)只能出现在赋值语句的右边,比如常量。
引用#
⼀般说的引用是指的左值引用
引用:引用是为对象起了另外⼀个名字,引用类型:引用(refer to)另外⼀种类型。
int &refVal = val;
- 引用必须初始化:
int &refVal2; //报错,引用必须被初始化
- 引用和它的初始值是绑定 bind在⼀起的,而不是拷贝。⼀旦定义就不能更改绑定为其他的对象
- 引用类型要与绑定对象匹配
- 引用只能绑定在对象上,不能与字面值或表达式计算结果绑定
更详细的解释可参考:C++ 中的引用
指针#
是⼀种 "指向(point to)" 另外⼀种类型的复合类型。 本身就是⼀个对象,无需定义时赋值
定义指针类型#
int *ip1,*ip2;
//ip1和ip2都是int型对象指针
double dp,*dp2
//dp2 是指向double型对象的指针,dp是double类型
获取对象的地址#
指针存放某个对象的地址。 获取对象的地址:
int i=42;
int *p = &i;
&
是取地址符。 指针的类型与所指向的对象类型必须⼀致(均为同一类型 int、double 等)
int ival = 42;
int *p = &ival; //p存放ival的地址,p是指向val的指针
double dval;
double *pd = &dval;//正确
double *pd2 = pd;//正确
int *pi = pd;//错误,类型不匹配
pi = &dval;//错误,试图把double对象的地址赋给int指针
指针的值 (即地址) 的四种状态:
- 指向⼀个对象;
- 指向紧邻对象的下⼀个位置;
- 空指针;
- 无效指针。
对无效指针的操作均会引发错误;
第二种和第三种虽为有效的,访问指针对象的行为后果无法预计。
指针访问对象#
#include <iostream>
int main() {
//如果指针指向一个对象,使用解引用符(操作符'*')来访问对象
int ival = 42;
int *p = &ival;
std::cout << *p;// 输出p指针所指对象的数据,
*p = 0;
std::cout << ival; // 0
return 0;
}
解引用操作仅适用于确实指向某个对象的有效指针。
空指针#
int *p1=nullptr; //使用空指针。
int *p2 = 0;//p2 初始化为字面常量0
// include<cstdlib>
int *p3 = NULL;//int *p3=0 NULL预处理变量
int zero = 0;
p1 = zero; //错误,不能把int变量直接赋给指针
建议初始化所有指针!
指针与引用的区别#
- 引用本身并非⼀个对象,引用定义后就不能绑定到其他的对象上了;
- 指针并没有此限制
//说明指针和引用的主要区别
//引用是另⼀个对象的别名,而指针本身就是⼀个对象。
//引用必须初始化,并且⼀旦定义了引用就无法再绑定到其他对象。
//而指针无须在定义时赋初值,也可以重新赋值让其指向其他对象。
int i = 42;
int *pi = 0;//pi 初始化,没有指向任何对象
int *pi2 = &i;//pi2指向i
int *pi3;//如果pi3定于于块内,pi3值无法确定
pi3 = pi2;//pi3与pi2指向同⼀对象i
pi2 = 0;//pi2 不指向任何对象
//赋值语句永远改变的是左侧的对象。
pi = &ival;//pi指向了ival
*pi = 0;//ival值改变,pi没改变
//请解释下述定义。在这些定义中有非法的吗?如果有,为什么?
int i = 0;
(a) double* dp = &i;
(b) int *ip = i;
(c) int *p = &i;
(a): ⾮法。不能将⼀个指向 double 的指针指向 int 。
(b): ⾮法。不能将 int 变量赋给指针。
(c): 合法。
其他指针操作#
int ival = 1024;
int *p1=0;
int *p2=&ival;
//指针值0,条件取false
if(p1)
//...
//p2指针⾮0,条件值是true
if(p2)
//...
//指针⽐较
if(p1==p2)
//...
非法指针会引发不可预计的后果!
//⼀条语句定义不同类型变量
int i=1024,
*p=&i,&r=i;
int* p;//合法但容易产⽣误导
int* p1,p2;//p1是指针,p2是int
int *p1,*p2;//p1,p2都是指针
或
int *p1;
int *p2;
指向指针的指针#
#include <iostream>
using namespace std;
int main() {
int ival = 1024;
int *pi = &ival; //pi指向int型
int **ppi = π //ppi指向int型指针
// 解引用
cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **ppi
<< endl;
return 0;
//The value of ival
//direct value: 1024
//indirect value: 1024
//doubly indirect value: 1024
}
指向指针的引用#
从右向左阅读 r 的定义
int i=42;
int *p;
int *&r = p; //r是对指针p的引用
r = &i; //p指向i
*r=0; //i=0
说明下列变量的类型和值。
(a) int* ip, i, &r = i;
(b) int i,*ip = 0;
(c) int* ip, ip2;
(a): ip 是⼀个指向 int 的指针, i 是⼀个 int, r 是 i 的引⽤。
(b): i 是 int , ip 是⼀个空指针。
(c): ip 是⼀个指向 int 的指针, ip2 是⼀个 int。
void 指针#
void
指针可以存放任意对象的地址。因无类型,仅操作内存空间,不能直接操作void *
指针所指的对象。
double obj = 3.14,
*pd = &obj;
void *pv = &obj; //obj 可以是任意类型的对象
pv = pd;//void* 可存任何类型,使用时再进行强制类型转换即可
CONST 限定符#
const:定义⼀些不能被改变值的变量, const 对象⼀旦创建值不再改变,所以必须初始化,且不能被改变。
const int bufSize = 512;
bufSize = 512;//error,试图写值到const对象
const int i= getSize();
const int j=42;
const int k;//error,k未初始化常量
int i=42;
const int ci=i;
int j=ci;
CONST 对象仅在文件内有效:
-
当多个文件出现同名的 const 变量,等同于在不同文件分别定义独立变量。
-
要想在多个文件中使用 const 变量共享,定义和声明都加 extern 关键字即可。
//file_1.cc定义初始化常量,能被其它文件访问
extern const int bufSize = fcn();
//file_1.h 头文件
extern const int bufSize ;//与file_1.cc中定义的bufSize是同⼀个
const int buf; // 不合法, const 对象必须初始化
int cnt = 0; // 合法
const int sz = cnt; // 合法
++cnt; ++sz; // 不合法, const 对象不能被改变
const 的引用#
- 把引用绑定到 const 对象上,称之为对常量的引用。
- 与普通引用不同,对常量的引用不能被用作修改它所绑定的对象
const int ci=1024;
const int &r1=ci;
r1=42; //error r1是对常量的引用
int &r2 = ci; //error,试图让非常量引用指向⼀个常量对象
初始化和对 const 的引用#
引用类型必须与其所用对象类型一致
- 例外:初始化常量引用时,允许用任意表达式做初始值,只要表达式的结果能转换成引用的类型。
- 允许为一个常量引用绑定非常量的对象,字面值,一个一般表达式
int i=42;
const int &r1=i; //允许将const int&绑定到int对象
const int &r2=42;//r2 是个常量引用
const int &r3=r1*2;//r3是个常量引用
int &r4=r1*2;//error,r4是⼀个普通的非常量引用。
//当⼀个常量引用被绑定到另外⼀种类型发生了什么?
double dval = 3.14;
const int &ri=dval;
//编译器将上述代码改为如下形式
const int temp=dval;//生成临时整型变量
const int &ri=temp;//让ri绑定这个临时变量
- 临时量 (temporary) 对象:当编译器需要一个空间来暂存表达式结果的值时,临时创建的一个未命名的变量
- 对临时量的引用是错误行为:
double dval = 3.14;
int &ri = dval; //error
对 const 的引用很可能引用一个非 const 对象
- 常量引用仅对引用可参与的操作做出限定,对引用的对象本身是不是常量未做限定
int i = 42;
int &r1 = i; //r1绑定i
const int &r2 = i; //r2绑定i,不允许通过r2修改i
r1 = 0; //ok,r1非常量
r2 = 0; //error,r2是一个常量引用
和常量引用⼀样,指向常量的指针同理也没有规定所指对象必须是常量。仅要求不能通过指针改变对象值。
const 指针#
常量指针必须初始化,允许把指针定义成常量:
int errNumb=0;
int *const curErr=&errNumb;// curErr是常量,⼀直指向errNumb(地址不能变)
const double pi=3.1415;
const double *const pip= π//pip是常量,*pip也是常量,
//pip 是指向常量对象的常量指针 (值与地址均不能更改)
*pip = 2.72;//error *pip 是常量
if(*curErr){
errorHandler();
*curErr=0;//ok *curErr不是常量
}
//下面的哪些初始化是合法的?请说明原因。
int i = -1, &r = 0; // 不合法, r 必须引用⼀个对象
int *const p2 = &i2; // 合法,常量指针
const int i = -1, &r = 0(常量引用); // 合法
const int *const p3 = &i2; // 合法
const int *p1 = &i2; // 合法
const int &const r2; // 不合法, r2 是引用,没有顶层const
const int i2 = i, &r = i; // 合法
//说明下面的这些定义是什么意思,挑出其中不合法的。
int i,*const cp; // 不合法, const 指针必须初始化
int *p1,*const p2; // 不合法, const 指针必须初始化
const int ic, &r = ic; // 不合法, const int 必须初始化
const int *const p3; // 不合法, const 指针必须初始化
const int *p; // 合法. ⼀个指针,指向 const int
//假设已有上⼀个练习中定义的那些变量,则下⾯的哪些语句是合法的?请说明原因。
i = ic; // 合法, 常量赋值给普通变量
p1 = p3; // 不合法, p3 是const指针,不能赋值给普通指针
p1 = ⁣ // 不合法, 普通指针不能指向常量
const int * const p3 = ⁣ // 合法, p3 是常量指针且指向常量
p3 = ⁣ //此时不合法 p3 常量
int * const p2 = p1; // 合法, 可以将普通指针赋值给常量指针
p2 = p1;//此时不合法 p2是常量
const int ic = *p3; // 合法, 对 p3 取值后是⼀个 int 然后赋值给 ic
ic = *p3;//此时不合法,ic是常量
顶层 const#
- 顶层 const:指针本身是个常量。
- 底层 const:指针指向的对象是个常量。拷贝时严格要求相同的底层 const 资格。
int i=0;
int *const p1=&i;//p1常量,顶层const
const int ci=42;//ci常量,顶层const
const int *p2=&ci;//*p2常量,底层const
const int *const p3=p2;//靠右顶层,靠左底层const
const int &r =ci;//用于声明引用的const都是底层const
//拷贝时,顶层const不受影响
i=ci;//拷贝ci的值,ci顶层const,⽆影响
p2=p3;//p2 p3所指对象类型相同 p3顶层const部分不影响
//拷贝时严格要求相同的底层const资格,数据类型能够转换。
int *p =p3;//error p3包含底层const定义,p没有
p2=p3;//ok p2 p3都是底层const
p2=&i; //ok int* 转const int*
int &r=ci;//error 普通int& 不能绑定到int 常量
const int &r2=i;//ok const int& 可以绑定到int
//对于下面的这些语句,请说明对象被声明成了顶层const还是底层const?
const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2,
*const p3 = &i, &r2 = v2;
//v2 是顶层const,p2 是底层const,p3 既是顶层const⼜是底层const,r2 是底层const。
//假设已有上⼀个练习中所做的那些声明,则下面的哪些语句是合法的?
//请说明顶层const和底层const在每个例子中有何体现。
r1 = v2; // 合法, 顶层const在拷贝时不受影响
p1 = p2; // 不合法, p2 是底层const,如果要拷贝必须要求 p1 也是底层const
p2 = p1; // 合法, int* 可以转换成const int*
p1 = p3; // 不合法, p3 是⼀个底层const,p1 不是
p2 = p3; // 合法, p2 和 p3 都是底层const,拷贝时忽略掉顶层const
constexpr 和常量表达式#
- 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。
//⼀个对象(表达式)是不是常量表达式由数据类型和初始值共同决定
const int max_file=30; //max_file是常量表达式
const int limit = max_file+1; //limit是常量表达式
int staff_size = 7; //staff_size不是常量表达式
const int sz = get_size(); //运⾏时获取,sz是常量表达式。
- C++11 新标准规定,允许将变量声明为
constexpr
类型以便由编译器来验证变量的值是否是⼀个常量的表达式。
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf+1; //mf+1是常量表达式
constexpr int sz = get_size(); //取决于get_size()是否是constexpr函数
字面值类型#
常量表达式的值编译时就得到计算,类型简单,值容易 得到,称为为 “字面值类型”(literal type)
- 算数类型
- 引用,指针,nullptr,0 或固定位置
指针和 constexpr#
- constexpr 把所定义的对象置为顶层
const int *p=nullptr;//p是⼀个指向常量的指针
constexpr int *q=nullptr;//q是常量指针
//constexpr把所定义的对象置为顶层
constexpr int *nq=nullptr;//nq是常量指针
int j=0;
constexpr int i=42;//i是常量
//i,j定义在函数外
constexpr const int *p=&i;//p是常量指针,指向整型常量i
constexpr int *p1=&j;//p1是常量指针,指向整数j
处理类型#
- 程序越来越复杂,类型越来越复杂。类型难于拼写,明确目的含义,搞不清需要什么类型。
类型别名#
-
传统别名:使用
typedef
来定义类型的同义词。typedef double wages; wages hour,weekly; //double hour,weekly;
-
新标准别名:别名声明(alias declaration):
using SI = Sales_item; //(C++11) SI items;//Sales_item items;
指针,常量和类型别名#
// 对于复合类型(指针等)不能代回原式来进行理解
typedef char *pstring; // pstring是char*的别名
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps;//ps是⼀个指针,对象是指向char的常量指针
//基本数据类型是指针而不是const char。
AUTO 类型说明符 (C++ 11)#
清楚的知道表达式类型并不容易,因此引入auto
。
- auto 类型说明符:让编译器自行推断类型。显然 auto 定义变量必须要有初始值。
- 一条生命一句只能有一种基本数据类型:
auto sz = 0, pi =3.14;//错误,类型不⼀致
复合类型,常量和 auto#
int i = 0, &r = i;
auto a = r; //推断a类型是int
//会忽略顶层const
const int ci = i;
auto b = ci;//推断int,忽略顶层const
const int ci = 1; const auto f = ci;
//推断类型是int,如果希望是顶层const需要⾃⼰加const
DECLTYPE 类型提示符 (C++ 11)#
decltype
是一个 C++11 中引入的关键字,用于获取表达式的类型。它的语法如下:
decltype(expression)
其中,expression
是一个表达式,可以是变量、函数调用、类型转换等。
decltype
的作用是根据表达式的类型推断出一个变量或表达式的类型,并且返回该类型。例如:
int x = 42;
decltype(x) y; // y 的类型为 int
double square(double value) {
return value * value;
}
decltype(square(3.14)) result; // result 的类型为 double
struct Point {
int x;
int y;
};
Point p;
decltype(p.x) a; // a 的类型为 int
在上面的示例中,decltype(x)
返回 int
类型,因为表达式 x
的类型是 int
。decltype(square(3.14))
返回 double
类型,因为表达式 square(3.14)
的返回类型是 double
。decltype(p.x)
返回 int
类型,因为 p.x
是 Point
结构体的一个成员变量。
decltype
在编写泛型代码时非常有用,可以根据参数的类型来推断出函数的返回类型,避免手动指定返回类型。例如:
template<typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b;
}
int main() {
int x = 5;
double y = 3.14;
auto result = multiply(x, y); // result 的类型为 double
return 0;
}
在上面的示例中,multiply
函数使用 decltype(a * b)
来推断返回类型,根据传入的参数类型,返回类型会自动推断为 double
。
decltype
是一个非常有用的关键字,可以让编译器根据表达式的类型来推断出变量或函数的类型,提高代码的灵活性和可读性。
-
从表达式的类型推断出要定义的变量的类型。
-
decltype:选择并返回操作数的数据类型,不计算表达式的值。
decltype(f()) sum = x; //推断sum的类型是函数f的返回类型。
- 不会忽略顶层 const。
- 如果对变量加括号,编译器会将其认为是⼀个表达式,
decltype((i))
得到结果为引用。 - 赋值是会产⽣引用的⼀类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。
自定义数据结构 STRUCT#
尽量不要把类定义和对象定义放在⼀ 起。如 struct Student {} xiaoming,xiaofang;
- 类可以以关键字 struct 开始,紧跟类名和类体。
- 类数据成员:类体定义类的成员。
- C++11:可以为类数据成员提供⼀个类内初始值 (in-class initializer)。
下面举例:
- 定义 Sales_data 类:
//Sales_data.h
#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 类
//Sales_data.cpp
#include <iostream>
#include <string>
#include "Sales_data.h"
int main() {
Sales_data data1, data2;
// code to read into data1 and data2
double price = 0; // price per book, used to calculate total revenue
// read the first transactions: ISBN, number of books sold, price per b
std::cin >> data1.bookNo >> data1.units_sold >> price;
// calculate total revenue from price and units_sold
data1.revenue = data1.units_sold * price;
// read the second transaction
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;
// code to check whether data1 and data2 have the same ISBN
// and if so print the sum of data1 and data2
if (data1.bookNo == data2.bookNo) {
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;
// print: ISBN, total sold, total revenue, average price per book
std::cout << data1.bookNo << " " << totalCnt
<< " " << totalRevenue << " ";
if (totalCnt != 0)
std::cout << totalRevenue / totalCnt << std::endl;
else
std::cout << "(no sales)" << std::endl;
return 0; // indicate success
} else { // transactions weren't for the same ISBN
std::cerr << "Data must refer to the same ISBN"
<< std::endl;
return -1; // indicate failure
}
}
编写自己的头文件#
- 头文件通常包含一些只能被定义⼀次的实体:类、 const 和 constexpr 变量。
预处理器概述:
- 预处理器(preprocessor):确保头文件多次包含仍能安全⼯作。
- 当预处理器看到 #include 标记时,会用指定的头文件内容代替 #include
- 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
- #indef 已定义时为真
- #inndef 未定义时为真
- 头文件保护符的名称需要唯⼀,且保持全部大写。养成良好习惯,不论是否该头文件被包含,都要加保护符。
#ifndef SALES_DATA_H //SALES_DATA_H未定义时为真
#define SALES_DATA_H
strct Sale_data{
...
}
#endif