exiler

exiler

C++ 入门-1

基本内置类型#

不同的编译器和平台可能会有差异,下表为在 Windows_X64 环境下使用 MinGw GCC 9.2.0 32_bit 为例:

类型含义最小尺寸范围
bool布尔类型8bitstrue 或 false
char字符型8bits-2^7 ~ 2^7-1
wchar_t宽字符16bits0 ~ 2^16 - 1
char16_tUnicode 字符16bits0 ~ 2^16 - 1
char32_tUnicode 字符32bits0 ~ 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 表现为带符号还是无符号的,由编译器决定

如何选择类型#

  1. 数值不可能是负数时,比如年龄,长度等等,选用无符号类型
  2. 整数计算式使用int类型,一般long的大小和int一样,而short显得太小。如果处理较大的整数,应该选择long long
  3. 算数表达式中不要使用boolchar,符号容易出问题;
  4. 浮点数据运算使用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)#

  1. 整型字面值常量:
  • 十进制 :42
  • 八进制: 052 (以 0 开头)
  • 十六进制: 0x2A 或 0X2A (以 0x 或 0X 开头)
  • 二进制: 0b1010100B101010(以 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;
}
  1. 浮点型字面值常量:
  • 十进制:3.142.0
  • 科学计数法:3.0e8(表示 3.0 乘以 10 的 8 次方)
  1. 字符型字面值常量:
  • 字符:'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;
}
  1. 字符串字面值常量:字符串字面值常量表示一串字符,使用双引号括起来。例如:
  • 字符串:"Hello, World!"

字符串实际类型是字符数组,结尾添加 '\0' 字符 字符串型实际上是常量字符构成的数组, 结尾处以 '\0' 结束,所以字符串类型实际 上长度比内容多 1。

  1. 布尔字面值常量:布尔字面值常量表示真或假,可以是 truefalse

  2. 指针字面值: nullptr

  3. 指定字面值类型:

字符和字符串字面值:

前缀含义类型
uUnicode16 字符char16_t
UUnicode32 字符char32_t
L宽字符wchar_t
u8UTF-8 (字符串字⾯常量)char

整型字面值:

后缀最小匹配类型
u or Uunsigned
l or Llong
ll or LLlong long

浮点型字面值:

后缀类型
f or Ffloat
l or Llong 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;  //声明

变量标识符与命名规范#

标识符:

  1. 由字母、数字、下划线组成,必须以字母或下划线开头。
  • 长度没有限制。

  • 大小写写敏感。

  1. 保留名字不能用做标识符。

  2. 用户自定义标识符

  • 不能连续出现两个下划线
  • 不能以下划线紧跟大写字母开头
  • 定义在函数体外的标识符不能以下划线开头
  • 使用小驼峰命名法
//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

命名规范:

  1. 体现实际含义
  2. 变量名和函数名使用小驼峰命名法,如myVariable
  3. 自定义类名大写字母开头,如Sales_item
  4. 若标识符有多个单词组成,单词之间要有明显区分,如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; 
  1. 引用必须初始化:
 int &refVal2;  //报错,引用必须被初始化 
  1. 引用和它的初始值是绑定 bind在⼀起的,而不是拷贝。⼀旦定义就不能更改绑定为其他的对象
  2. 引用类型要与绑定对象匹配
  3. 引用只能绑定在对象上,不能与字面值或表达式计算结果绑定

更详细的解释可参考: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指针

指针的值 (即地址) 的四种状态:

  1. 指向⼀个对象;
  2. 指向紧邻对象的下⼀个位置;
  3. 空指针;
  4. 无效指针。

对无效指针的操作均会引发错误;

第二种和第三种虽为有效的,访问指针对象的行为后果无法预计。

指针访问对象#

#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 = &pi; //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= &pi;//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 = &ic; // 不合法, 普通指针不能指向常量

const int * const p3 = &ic; // 合法, p3 是常量指针且指向常量
p3 = &ic; //此时不合法 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 的类型是 intdecltype(square(3.14)) 返回 double 类型,因为表达式 square(3.14) 的返回类型是 doubledecltype(p.x) 返回 int 类型,因为 p.xPoint 结构体的一个成员变量。

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
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。