从零开始学C++(1 变量和基本类型)

0
10

  接下来的几篇文章介绍C++的基础知识点。

  C++是一种静态数据类型语言,它的类型检查发生在编译时。因此,编译器必须知道程序中每一个变量对应的数据类型。

  数据类型是程序的基础:它告诉我们数据的意义以及我们能在数据上执行的操作。

  比如:i = i + j; 这条语句的具体含义要取决于i、j的类型

  

  void也是一种类型,即空类型。

  算术类型:整型(integral type,包括字符和布尔类型)、浮点型

  算术类型所占的位(bit)数,在不同机器上有所差别。C++标准规定了类型的最小尺寸,比如int最小占16位,char最小占8位,long最小占32位,long long最小占64位,float最小6位有效数字,double最小10位有效数字等等。

  布尔类型(bool)的取值是真(true)或者假(false)。

  基本的字符类型是char,一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。其他字符类型用于扩展字符集,如wchar_t、char16_t、char32_t。wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,char16_t和char32_t则为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。

  

   简单介绍一下内置类型的机器实现:

    计算机是以二进制形式存储数据的,也就是每个位非0即1。一般计算机都以2的整数次幂个bit作为块来处理内存,大多数以 8 bits 构成一个最小的可寻址内存块,称之为一个字节(byte)。

    而计算机将内存中的每个字节与一个数字(也就是地址)关联起来,例如:

    

    图中,左侧的数字是该字节的地址,右侧是 8 bits的具体内容。

    使用某个地址来表示从这个地址开始的大小不同的比特串,例如,地址1000的那个字,地址1003的那个字节。由此可见,为了明确某个地址的含义,必须首先知道存储该地址的数据的类型,也就是需要知道数据所占的位数以及该如何解释这些bit的内容。

  带符号类型(signed)和无符号类型(unsigned)

    类型int、short、long和long long都是带符号的,类型名前添加unsigned得到无符号类型。

    字符型被分为了三种:char、signed char和unsigned char。特别需要注意的是:类型char和类型signed char并不一样。类型char实际上会表现为带符号的和无符号的两种形式中的一种,具有是哪种由编译器决定。

  类型转换(convert):将对象从一种给定的类型转换为另一种相关类型。

  如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果:

int val1 = 10;
unsigned val2 = -20;
std::cout << val1 + val2 << std::endl;

  这段代码得到的结果就不是我们想要的,因为结果会自动转换为无符号数。

  字面值常量

    字面值常量的形式和值决定了它的数据类型。例如: 20 /*十进制整型字面值*/ 024 /*八进制*/ 0x14 /*十六进制*/

    十进制字面值的类型是int、long和long long 中尺寸最小的那个,前提是这种类型能容纳下当前的值。

    short类型没有对应的字面值。

    如果我们使用了一个形如 -20 的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。

    ’a’ // char型字面值(字符字面值)

    ”hello c++” // 字符串字面值

    字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(’\0’),因此,字符串字面值的实际长度要比它的内容多1。

  转义序列:以反斜线开始,比如 \n(换行符) \r(回车符)

  在程序中,转义序列被当作一个字符使用。

  变量

    变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。

    初始化(initialized):对象被创建时获得一个特定的值。

    列表初始化:

      int value = {0};

      int value2{0};

      当用于内置类型的变量时,列表初始化形式有一个重要特点:如果初始值存在丢失信息的风险,编译器将报错:long double ld = 3.14; int a{ld}, b = {ld}; // 错误,存在丢失信息的风险

  

  声明和定义:声明(declaration)使得名字可被程序知道,一个文件如果想使用别处定义的名字则必须包含对该名字的声明。定义(definition)负责创建与名字关联的实体。

    extern int i; // 声明i

    int j; // 声明并定义j

    extern double pi = 3.1416; // 定义,包含了显示初始化

  作用域(scope):同一个名字在不同的作用域中可能指向不同的实体。

  作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。

  

  复合类型(compound type)是指基于其他类型定义的类型。比如:引用和指针。

  一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

  引用(reference)为对象起了另一个名字:

    int ival = 1024;

    int &ref_val = ival;

    int &ref_val2; // 错误,引用必须被初始化

    int &ref_val3 = 10; // 错误,初始值必须是一个对象

    double dval = 3.14;

    int &ref_val4 = dval; // 错误,初始值类型必须匹配

  指针(pointer)是“指向(point to)”另外一种类型的复合类型。指针本身是一个对象,而引用本身不是一个对象,它只是一个别名。

  空指针(null pointer)不指向任何对象,可以用字面值nullptr来指定。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。

  void*是一种特殊的指针类型,可用于存放任意对象的地址。但是,不能直接操作void*指针所值的对象,因为并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

// 指向指针的指针:
int ival = 1024; int *pi = &ival; int **ppi = &pi;

// 指向指针的引用:
int *&rpi = pi;
rpi = &ival;
*rpi = 0;

  const限定符

    const常量的特征仅仅在执行改变该常量的操作时才会发挥作用。

    当以编译时初始化的方式定义一个const对象时,如:

      const int buff_size = 512;

    编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行替换操作,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

  如果要在文件间共享,只在一个文件中定义const,在其他对各文件中声明并使用它。解决方法是,对于const变量不管是声明还是定义都添加extern关键字:

    // file1.cpp定义并初始化

    extern const int buff_size = func();

    // file1.h头文件

    extern const int buff_size; // extern的作用是指明buff_size并非本文件所独有

  对常量的引用:

    const int ci = 1024;

    const int &r1 = ci;

    r1 = 5; // 错误,r1是对常量的引用

    int &r2 = ci; // 错误,试图让一个非常量引用指向一个常量对象

  初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可:

    int i = 5;

    const int &r1 = i;

    const int &r2 = 1024;

    const int &r3 = r1 * 2;

    int &r4 = r1 * 2; // 错误,r4为非常量引用,初始值必须类型匹配

  一定要注意区分指向常量的指针(pointer to const)和常量指针(const pointer)。

  前者表示指针所指向的对象是常量,后者表示指针本身是常量。

    const double pi = 3.14;

    double *ptr = &pi; // 错误

    const double *cptr = &pi; // 正确,cptr就是指向常量的指针

    int *const pi = &i; // 常量指针,必须初始化

  这里引出:顶层const(top-level const)表示指针本身是个常量,底层const(low-level const)表示指针所指的对象是一个常量。

    const int &r = ci; // 用于声明引用的const都是底层const

  常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。

  一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

    const int max_files = 30; // max_files是常量表达式

    int staff_size = 27; // 不是常量表达式

    const int sz = GetSize(); // sz不是常量表达式,因为它的值直到运行时才能获取到 

  将变量声明为constexpr类型,以便编译器来验证变量的值是否是一个常量表达式:

    constexpr int mf = 30;

    constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句

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

    constexpr int *q = nullptr; // q是一个指向整型的常量指针

  类型别名(type alias)是一个名字,它是某种类型的同义词:

    typedef double wages; // wages是double的同义词

    using SI = SalesItem; // SI是SalesItem的同义词(C++11)

  

  从C++11开始,引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。

    auto i = 0, *p = &i; // 正确,数据类型一致

    auto sz = 0, pi = 3.14; // 错误,sz和pi的类型不一致

  编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。

    int i = 0, &r = i;

    auto a = r; // a是一个整数

    const int ci = i, &cr = ci;

    auto b = ci; // b是一个整数

    auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)

  如果希望推断出的auto类型是一个顶层const,需要明确指出:

    const auto d = ci; // d是const int

  decltype类型说明符,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

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

    const int ci = 0, &cj = ci;

    decltype(ci) x = 0; // x的类型是const int

    decltype(cj) y = x; // y的类型是const int&, y绑定到变量x

  如果表达式的内容是解引用操作,则decltype将得到引用类型(因为解引用指针可以得到指针所指的对象,而且还能给这个对象赋值):

    int i = 42, *p = &i;

    decltype(*p) c; // 错误,c的类型为int&,必须初始化

  注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果加上了括号,编译器就会把它当成一个表达式,得到引用类型:

    decltype(i) d;

    decltype((i)) e; // 错误,e的类型是int&,必须初始化

  在《Effective C++》中,有这样两条建议:尽量以const,enum,inline替换#define(条款02)、尽可能使用const(条款03)。

  对于第一条:

例子:#define ASPECT_RATIO 1.653  记号名称ASPECT_RATIO也许从未被编译器看见:在预处理阶段就被替换掉了。
如果运用此常量但获得一个编译错误,那可能会带来困惑,因为错误信息也许会提到1.653而不是ASPECT_RATIO
解决办法就是用常量替换宏:
    const double kAspectRatio = 1.653;

定义常量指针(指针本身是常量),常量通常定义在头文件内(以便被不同的源码含入),因此有必要将指针声明为const。
  const char* const kAuthorName = "Scott Meyers";
这里,string对象更合适:
  const std::string kAuthorName("Scott Meyers");

class专属常量
为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member)。而为确保此常量至多只有一份实体,必须让它成为一个static成员:
class GamePlayer {
private:
  static const int kNumTurns = 5;  // 常量声明式
  int m_scores[kNumTurns];
};
上面的kNumTurns是声明式而非定义式。必须另外提供定义式:
  const int GamePlayer::kNumTurns;    // kNumTurns的定义,应该放在实现文件中,声明时已获得初值,因此定义时不可以再设初值

如果不想让别人获得一个pointer或reference指向某个整数常量,可以用enum替换const。
class GamePlayer {
private:
  enum {EM_NUM_TURNS = 5};
  int m_scores[EM_NUM_TURNS];
};

#define误用情况:实现宏,宏看起来像函数,但不会招致函数调用带来的额外开销
  // 以a和b的较大值调用函数f
  #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
带来的问题:
  int a = 5, b = 0;
  CALL_WITH_MAX(++a, b);  // a被累加两次
  CALL_WITH_MAX(++a, b + 10); // a被累加一次
取而代之的应该写出template inline函数
  template<typename T>
  inline void CallWithMax(const T &a, const T &b) // 由于不知道T是什么,所以采用pass by reference-to-const
  {
    f(a > b ? a : b);
  }

  

  对于第二条:

STL迭代器系以指针为根据塑造出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const指针)。如果希望迭代器所指的东西不可被改动(即希望STL模拟一个const T*指针),则使用const_iterator:
  std::vector<int> vec;
  const std::vector<int>::iterator iter = vec.begin();  // iter的作用像个T* const
  std::vector<int>::const_iterator kIter = vec.begin(); // kIter的作用像个const T*

类中不恰当的声明const成员函数的例子:
class CTextBlock {
public:
  char& operator[](std::size_t position) const // bitwise const声明,但其实不适当
  {
    return m_ptext[position];
  }
private:
  char *m_ptext;
};
重载的operator[]函数,被声明为const成员函数,但是却返回一个reference指向对象内部值。
operator[]实现代码并不更改m_ptext,于是编译器很开心地为operator[]产出目标码。但是:
  const CTextBlock kctb("Hello");  // 声明一个常量对象
  char *pc = &kctb[0];
  *pc = 'J';  // kctb现在的内容为"Jello"

mutable(可变的)释放掉non-static成员变量的bitwise constness约束

在const和non-const成员函数中避免重复
假设TextBlock内的operator[]不单只是返回一个reference指向某个字符,也执行边界检验(bounds checking)等:
class TextBlock {
public:
  const char& operator[](std::size_t position) const
  {
    ... // 边界检验
    ... // 日志记录数据访问(log access data)
    ... // 检验数据完整性(verify data integrity)
    return m_text[position];
  }
  char& operator[](std::size_t position)
  {
    ... // 边界检验
    ... // 日志记录数据访问(log access data)
    ... // 检验数据完整性(verify data integrity)
    return m_text[position];
  }
private:
  std::string m_text;
};
上面的代码中包含很多重复代码,以及伴随的编译时间、维护、代码膨胀等问题。就算将边界检验...等代码移到一个函数内,也会存在重复代码:函数调用、两次return语句等等。
真正应该做的是实现operator[]的机能一次并使用它两次,也就是说,必须令其中一个调用另一个。
将常量性转除,这里将返回值的const转除是安全的:
class TextBlock {
public:
  const char& operator[](std::size_t position) const
  {
    ...
    ...
    ...
    return m_text[position];
  }
  char& operator[](std::size_t position)
  {
    return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
  }
};
如果不执行static_cast转换,则会递归调用自己。
令const版本调用non-const版本以避免重复————不应该这样做。记住,const成员函数承诺绝不改变其对象的逻辑状态(logical state),non-const成员函数却没有这般承诺。

<

发布回复

请输入评论!
请输入你的名字