父进程

终生学习,自由生活


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

  • 搜索

c++系列文章(6):类型别名和自动类型

发表于 2019-12-21 | 分类于 C++ | | 阅读次数:
| 字数统计: 1.4k 字 | 阅读时长 ≈ 5 分钟

类型别名

  类型别名是一个名字,它是某种类型的同义词。使用类型别名可以使复杂的类型名字变得简单明了、易于使用。c++有两种方法用于定义类型别名:typedef和using。

  typedef

1
typedef double wages; //wages是double的同义词
2
typedef wages base,*p; //base是double的同义词,p是double*的同义词

  关键字typedef作为声明语句中的基本数据类型的一部分出现,含有typedef的声明语句定义的不再是变量而是类型别名。

  using

  c++11规定了新的别名声明关键字using。

1
using wages = double; //wagess是double的同义词
2
using p = *double; //p是double*的同义词,即指向double的指针

  声明成类型别名之后,其基本数据类型可能会发生改变,如果直接将类型别名替换成原本的样子进行理解,可能会发生错误。

1
using pstring = *char; //pstring表示指向字符的指针
2
const pstring p = nullptr; //p是一个常量指针,不能将其理解为const char*,即指向字符常量的普通指针

自动类型

  auto

  c++11引入了auto类型说明符,让编译器去分析表达式的初始值来推断类型,因此auto定义的变量必须具有初始值。

1
auto item = item1 + item2; //有item1和item2相加的结果推断出item的类型

  使用auto也能在一条语句中声明多个变量,因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一致。

1
auto i = 0,*p = &i; //正确,i是整数,p是整型指针
2
auto sz = 0,pi = 3.14; //错误,sz和pi的基本数据类型不一致

  编译器推断出来的auto类型可能和初始值的类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则:
  1、对于引用类型作为初始值,编译器以引用对象的类型作为auto的类型。

1
int i = 0,&r = i;
2
auto a = r; //a的类型为int

  2、auto一般会忽略顶层const,保留底层const

1
const int ci = 0,&cr = &ci; //i是一个整型常量,cr是一个常量引用
2
auto b = ci; //b是一个整数,ci的顶层const被忽略
3
auto c = cr; //c是一个整数,cr是ci的引用,而ci的顶层const被忽略
4
auto d = &ci; //d是一个指向整数常量的指针,对常量对象取地址是一种底层const
5
//如果希望推断出的auto类型是一个顶层const,需要明确指出
6
const auto e = ci; //ci的推演类型是int,而e的类型是const int

  如果将引用(或者指针)的类型设置为auto,初始值中的顶层const属性仍然保留,此时在引用(或者指针)中转化为了底层const。

1
auto &f = ci; //f是一个常量引用,绑定到ci
2
auto &g = 1; //错误,不能为非常量引用绑定字面值
3
const auto &h = 1; //正确,可以外常量引用绑定字面值

  要在一条语句中定义多个变量,需要切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型。

1
//auto的基本数据类型都是int,j是整型,k是整型引用
2
auto j = ci,&k = i; 
3
//auto的基本数据类型都是const int,m是对整型常量的引用,p是指向整型常量的指针
4
auto &m = ci,*p = &ci; 
5
//错误,i的基本数据类型是int,而ci是基本数据类型是const int
6
auto &n = i,*p = &ci;

  decltype

  如果希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,为了满足这类需求,c++11引入了decltype,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

1
decltype(f()) sum = x; //sum的类型就是函数f()的返回类型

  decltype处理顶层const和引用(或者指针)的方式与auto有所不同。
  1、如果decltype使用的表达式是一个变量,那么该变量是什么类decltype就返回什么类型(包括顶层const和引用)。

1
const int ci = 1,&cr = ci;
2
decltype(ci) x = 0; //x的类型是const int
3
decltype(cr) y = x; //y的类型是const int&,y绑定到x上
4
decltype(cr) z; //错误,z是一个引用,必须初始化

  注意:引用从来都是作为其所指对象的同义词出现的,只有用来decltype时是一个列外。
  2、如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。

1
int i = 1,*p = &i,&r = i;
2
decltype(r+0) b; //加法的结果是int类型,因此b的类型是int
3
decltype(*p) c; //错误,解引用的结果是引用类型,因此c的类型是int&,必须初始化

  如果decltype使用的表达式是一个加了括号的变量,编译器会把它当成是一个表达式,此时得到的是引用类型。

1
decltype((i)) d; //错误,d是int&,必须初始化
2
decltypr(i) e; //正确,e是int

c++系列文章(5):constexpr和常量表达式

发表于 2019-12-21 | 分类于 C++ | | 阅读次数:
| 字数统计: 820 字 | 阅读时长 ≈ 2 分钟

  常量表达式是指值不会改变并且在编译阶段就能得到计算结果的表达式,显然字面值属常量表达式,用常量表达式初始化的const对象也是常量表达式。
  一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。

1
//staff_size不是常量表达式,尽管staff_size的初始值是个字面值常量,但是它的数据类型只是一个普通的int而非const int。
2
int staff_size = 1; 
3
//sz不是常量表达式,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到
4
const int sz = get_size();
5
const int max_files = 1; //max_files是常量表达式
6
const int limit = max_files + 1; //limit是常量表达式

constexpr变量

  在一个复杂系统中,很难分辨一个初始值到底是不是常量表达式。因此c++11规定,允许将变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

1
constexpr int mf = 1; //1是常量表达式
2
constexpr int limit = mf + 1; //mf+1是常量表达式
3
constexpr int sz = size(); //只有当size是一个constexpr函数时,才是一条正确的声明语句。

  尽管不能使用普通函数作为constexpr变量的初始值,但c++11允许定义一种特殊的constexpr函数,这种函数足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。

字面值类型

  常量表达式的值需要在编译时就得到计算,因此对声明为constexpr时用到的类型必须有所限制,因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。
  常见的算术类型、引用和指针都属于字面值类型,自定义的类类型、IO库、string类型则不属于字面值类型,也就不能被定义为constexpr。
  尽管指针和引用能够定义为constexpr,但它们的初始值却受到严格限制,一个constexpr指针的初始值必须是nullptr或者0,或者是存储与某个固定地址中的对象。函数体内的变量一般来说并非存放在固定地址中,因此constexpr不能指向这样的变量。相反,定义在所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。此外c++允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此constexpr引用能够绑定到这样的变量之上,constexpr指针也能指向这样的变量。

指针和constexpr

  在constexpr声明中如果定义了指针,constrxpr限定符只对指针有效,与指针所指的对象无关。

1
const int* p = nullptr; //p是一个指向整型常量的指针
2
constexor int* q = nullptr; //q是一个指向整型的常量指针

c++系列文章(4):const限定符

发表于 2019-12-10 | 分类于 C++ | | 阅读次数:
| 字数统计: 1.5k 字 | 阅读时长 ≈ 5 分钟

初始化

  const对象一旦创建后其值就不能再改变,所以const对象必须初始化,且初始值可以是任意复杂的表达式。

1
const int i = getsize(); //运行时初始化
2
const int j = 1; //编译时初始化

  当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方替换成对应的值。为了执行替换,编译器必须知道常量的初始值,因此默认情况下,const对象被设定在仅在文件内有效,当在多个文件中出现了同名的const变量时,其实等同于在不同的文件中分别定义了独立的变量。
  某些时候,我们不希望编译器为每个文件分别生成独立的变量,而是希望像普通变量一样在不同文件之间共享const变量,也就是说只在一个文件中定义const,而在其他多个文件中声明并使用它。解决的方式是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次即可。

1
//file_1.cc
2
extern const int bufSize = fcn();
1
//file_1.h
2
extern const int bufSize; //与file_1.cc中定义的bufSize是同一个

const和引用

  说明:程序员常把对const的引用简称为“常量引用”。
  之前提到过,引用的类型必须与其所引用对象的类型一致,但有两个例外。第一种例外就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值、甚至一个一般表达式。

1
int i = 1;
2
const int &r1 = i; //正确,允许将常量引用绑定到一个普通变量上
3
const int &r2 = 1; //正确,允许将常量引用绑定到一个字面值上
4
const int &r3 = i*2; //正确,允许将常量引用绑定到一般表达式上
5
int &r4 = i*2; //错误,r4是普通的非常量引用,不能绑定到一个表达式上
6
double d = 3.14;
7
const int &r5 = d; //正确,double变量能够转换成int类型

  为什么能够将一个普通double类型变量绑定到一个int类型的const引用上?实际上,当一个常量引用被绑定到另外一种类型上时,这个常量引用实际是绑定在了一个临时变量上。

1
const int temp = d; //由double数生成一个临时的int变量
2
const int &r5 = &temp; //r5实际是绑定在这个临时变量上

  常量引用只是对引用可参与的操作做出了限定,对与引用的对象本身发是不是一个常量并未限制。因此常量引用既可以绑定const对象也可以绑定非常量对象,如果绑定的是非常量对象,则只是不能通过引用改变该对象,但可以直接通过该对象进行改变。

1
int i = 1;
2
const int &r = i; //正确,常量引用可以绑定到非const对象上
3
r = 2; //错误,不能通过常量引用改变绑定对象的值
4
i = 2; //正确,i是非const对象,可以改变其值

const和指针

  指向常量的指针

  和常量引用类似,指向常量的指针不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。

1
const int i = 1;
2
int *p1 = &i; //错误,p1是一个普通指针,不能存放常量对象的地址
3
const int *p2 = &i; //正确,只能用指向常量的指针存放常量对象的地址
4
*p2 = 2; //错误,指向常量的指针不能用于改变其所指对象的值

  之前提到过,指针的类型必须与其所指向对象的类型一致,但有两个例外。第一种例外就是允许一个指向常量的指针指向一个非常量对象。

1
int i = 1;
2
const int *r = &i; //正确

  和常量引用类似,指向常量的指针只是规定不能通过指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

  常量指针

  指针是对象而引用不是,因此指针也能像其他类型一样,将指针本身定义为常量。常量指针必须初始化,而且一旦初始化,则它的值(也就是存放在指针中的那个地址)就不能改变了。把*放在const之前用以说明指针是一个常量。

1
int i = 0;
2
int *const  p1 = &i; //p1将一直指向i
3
const int j = 0;
4
const int *const p2 = &j; //p2是一个指向常量的常量指针

  为了区分指针的const的属性,用顶层const表示指针本身是一个常量,用底层const表示指针所指的对象是一个常量。
  当执行对象的拷贝时,常量是顶层const还是底层const区别明显。对于顶层const而言,由于拷贝操作不会改变被拷贝对象的值,因此拷入或者拷出的对象是否是常量都没有影响。如果是底层const,则拷入和拷出的对象必须具有相同的底层const属性,或者两个对象的数据类型必须能够转换,一般来说非常量能够转成常量,反之则不行。

1
const int i = 0;
2
const int *p1 = &i;
3
int *p2 = p1; //错误,p1包含底层const属性,而p2则不包含
4
int j = 0;
5
int *p3 = &j;
6
cosnt int *p4 = p3; //正确,非常量能够转换成常量

c++系列文章(3):复合类型——指针和引用

发表于 2019-12-08 | 分类于 C++ | | 阅读次数:
| 字数统计: 1.2k 字 | 阅读时长 ≈ 4 分钟

  复合类型是指基于其他类型定义的类型,本文主要介绍其中的两种:指针和引用(这里是指左值引用)。

声明符

  最简单的声明语句由一个数据类型和紧随其后的一个变量名列表组成,但更通用的描述是,一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
  在同一条声明语句中,基本数据类型只有一个,但是声明符却可以有多种形式,也就是说一条声明语句可以定义处不同类型的变量。

1
//这条声明语句的基本数据类型是int,但是由于声明符不同,i、p、r这三个变量的类型也不同
2
int i = 1024, *p = &i, &r = i; //i是int型的数,p是int型指针,r是一个int型引用

  基本数据类型和类型修饰符(如*和&)的关系常常让人迷惑,其实类型修饰符就是声明符的一部分,而且同一条声明语句中的类型修饰符只作用于其紧接着的变量,而不是声明语句中的所有变量。

1
//基本数据类型是int,而不是int*,*号只作用于p
2
int *p = &i,q; //p是指向int的指针,q是int

  对于复杂的声明语句,要理解变量的类型是什么,最简单的方法就是从右向左阅读变量的定义,离变量名最近的符号对变量的类型有最直接的影响。

1
int *p = &i;
2
//离r最近的符号是&,,因此r是引用类型,剩下的int *用于表示引用的类型是int型指针
3
int *&r = p;

引用

  除了两种例外情形外,引用的类型要和与之绑定的对象严格匹配,而且引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起。

1
int &refInt = 4; //错误:引用类型的初始值必须是一个对象
2
double Pi = 3.14;
3
int &refPi = pi; //错误:引用类型的初始值必须是int类型

指针

  除了两种例外情形外,指针的类型要和它所指向的对象严格匹配。

1
int* p = π //错误:试图把double类型对象的地址赋给int类型的指针

  空指针表示不指向任何对象,得到空指针最简单的方法就是使用字面值nullptr来初始化指针,nullptr是一种特殊类型的字面值,它可以转换成任意其他的指针类型,也可以通过将指针初始化为字面值0来生成空指针,但不能使用值为0的int类型的变量来初始化。

1
int *p = nullptr;
2
int *q = 0;
3
int i = 0;
4
int *pi = i; //错误,不能把int变量直接赋给指针

  只要指针拥有一个合法值,就能将它用在条件表达式中,如果指针的值是0,条件值取false,如果非0条件值取true。使用非法指针会引发不可预计的后果。

1
int *p = nullptr;
2
if(p) //p的值是0,条件的值是false
3
int i = 0;
4
int *pi = &i;
5
if(pi) //pi的值不是0,条件的值是true

  void*指针是一种特殊的指针,可以用于存放对象的地址,但和普通指针不同的是,我们对于该地址中是什么类型的对象并不了解。

1
int i = 0;
2
void *p = &i; //void*指针可以存放任意类型的对象地址
3
double d = 0.0;
4
p = &d; //void*指针可以存放任意类型的对象地址

  因此void指针所能做的事也有限:拿它和别的指针进行比较、作为函数的输入输出、赋给另外一个void指针,但不能直接操作void*指向的对象,因为我们无法确定该对象的类型,也就无法确定能在这个对象上做哪些操作。

区别与联系

  指针和引用都能提供对其他对象的间接访问,二者最主要的区别是引用本身并非一个对象,而指针是一个对象。
  一旦定义了引用,就无法令其再绑定其他对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
  而指针则没有这种限制,和其他对象一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。

c++系列文章(2):分离式编译——变量的声明和定义

发表于 2019-12-06 | 分类于 C++ | | 阅读次数:
| 字数统计: 471 字 | 阅读时长 ≈ 1 分钟

  C++语言支持分离式编译,该机制允许将程序分割成若干个文件,每个文件可被独立编译。如果将程序分为多个文件,则需要在文件之间共享代码,例如一个文件中的代码可能需要使用另一个文件中定义的变量,如cin和cout在标准库中定义,我们却能在自己的程序中使用。

声明和定义

  为了支持分离式编译,C++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义则负责创建与名字关联的实体。
  声明规定了变量的类型和名字,而定义除了规定了变量的类型和名字外,还申请了存储空间,还可能为变量赋一个初始值。如果只是声明一个变量而不是定义它,就在变量名前添加关键字extern并且不要显式初始化变量。

1
extern int i; //声明而非定义i
2
int j; //声明并定义j

  任何包含了显式初始化的声明即成为定义,不论是否有extern关键字。

1
extern int j = 0; //定义j

  变量只可以被定义一次,但可以被声明多次。

标识符的命名规范

  C++的标识符由字母、数字和下划线组成,其中必须以下划线或者字母开头。为了避免和标准库中的名字冲突,用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧接大写字母开头,此外定义在函数体外的标识符不能以下划线开头。

c++系列文章(1):signed和unsigned

发表于 2019-12-06 | 分类于 C++ | | 阅读次数:
| 字数统计: 856 字 | 阅读时长 ≈ 3 分钟

  带符号类型(signed)可以表示正数、负数或0,无符号类型(unsigned)仅能表示大于等于0的值。C++标准并没有规定signed类型应该如何表示,但约定了在表示范围内正值和负值的量应该平衡。在计算机内部,signed类型二进制的最高位是符号位,用来表示正负,其余二进制位用来表示数值;unsigned类型的所有二进制位都用于表示数值。

类型 最小值 最小值 大小
char -128(-2^7) 127(2^7-1) 1字节
signed char -128(-2^7) 127(2^7-1) 1字节
unsigned char 0 255(2^8-1) 1字节
short -32768(-2^15) 32767(2^15-1) 2字节
unsigned char 0 65535(2^16-1) 2字节
int -2^31 2^31-1 4字节
unsigned int 0 2^32-1 4字节
long -2^63 2^63-1 8字节
unsigned long 0 2^64-1 8字节

signed的负数范围比正数范围多1

  为什么signed类型的负数范围总是比正数范围多一个,这是因为在计算机内部存储时并不是直接采用原码形式,而是采用补码的形式。下面以signed char为例进行说明。
  不使用原码,是因为计算机只会做加法,不会做减法,例如1-1在计算机内部使采用1+(-1)的形式进行计算的。如果直接采用原码计算得到的结果是-2,为了避免原码减法运算错误的问题,引入了反码。正数的反码是其原码本身,负数的反码是其原码除符号位外全部取反。
  但反码又会带来新的问题,-0和+0都表示0,+0的反码为00000000,-0的反码为11111111,但这两个不同的反码都表示0,为了避免出现两个0的表现形式,引入了补码。正数的补码使其原码本身,负数的补码是其反码加1。此时+0的补码为0000 0000,-0的补码为1111 1111加1即1 0000 0000,由于signed char是8位,因此取低8位即为0000 0000,这样+0和-0的补码都是同一个形式。 但此时-0的原码1000 0000则没有作用了,而-128的原码1 1000 0000的低8位正好和-0的原码1000 0000相同,为了提供利用率,便用-0的原码来表示-128,cout << (int)((char)(0x80)); //输出-128。

注意事项

  char和bool类型不用用于算术表达式,char类型在某些机器上表现为unsigned char,在另一些机器上表现为signed char(在本环境下表现为signed char),为了代码易移植,在使用char是最好明确是unsigned还是signed。
  赋给unsigned类型一个超出它范围的值时,结果是初始值对unsigned类型表示数值的总数取模后的余数,如unsigned char c = -1; //c的值是-1%256,即255。
  赋给signed类型一个超出它范围的值时,结果是未定义的,在本环境下,signed char c = -130; //c的值是126,signed char c = 129; //c的值是-127。
  当表达式中既有unsigned类型又有signed类型时,signed类型都会转换成unsigned类型,因此不能将unsigned和singned类型在同一个表达式中混用。
  无论从unsigned类型中减去一个多大的值,最后的结果都不可能是负数。

说明

发表于 2019-12-04 | | 阅读次数:
| 字数统计: 100 字 | 阅读时长 ≈ 1 分钟

  大家好,这是我的个人博客,主要记载一些自己的学习和工作心得,偶尔也会写一些生活琐事,希望大家多多指教,共同进步,谢谢!
  为了避免误解,博客中的文章统一基于以下环境,特此说明。

属性 参数
系统 Ubuntu 16.04.6 LTS x86_64
cpp编译器 g++ 5.4.0

Juncheng Hu

父进程的个人博客,主要涉及C++、Linux、服务端、深度学习等方面

7 日志
1 分类
17 标签
GitHub E-Mail
© 2019 Juncheng Hu