C++基础知识要点--变量和基本类型
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中检查类型的过程称为类型检查(type checking).
事实上在C++语言中,初始化和赋值是两个完全不同的操作。
- 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
变量能且只能被定义一次,但是可以被多次声明。
(1)、引用(不是对象) 和指针(对象):
代码实例:
#include <iostream>
int main()
{
int i = 42;
std::cout << "i为:" <<i << std::endl;
int &r = i;
std::cout << "r为:" << r <<std::endl;
int *p;//p为所指地址,*p为该地址所存得值
p = &i;
std::cout << "p为:" << p << std::endl;
std::cout << "*p为:" << *p << std::endl;
*p = i;
std::cout << "p为:" << p << std::endl;
std::cout << "*p为:" << *p << std::endl;
int &r2 = *p;
std::cout << "r2为:" << r2 << std::endl;
}
运行结果:
在新标准型下,现在的C++程序最好使用 nullptr 或 0 进行初始化,同时尽量避免使用NULL。
- 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
int zero = 0;
pi = zero; //错误:不能把int变量直接赋给指针
-
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个 void * 指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
-
利用void* 指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void* 指针。不能直接操作 void* 指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
指向指针的指针:
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
- 通过 * 的个数可以区分指针的级别。也就是说,** 表示指向指针的指针,*** 表示指向指针的指针的指针,以此类推:
int ival =1024;
int *pi = &ival; //pi指向一个int型的数
int **ppi = π // ppi指向一个int型的指针
此处pi是指向int型数的指针,而ppi是指向int型指针的指针,下图描述了它们之间的关系。
解引用int型指针会得到一个int型的数,同样,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用:
指向指针的引用:
- 引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i = 42;
int *p ; //p是一个int型指针
int *&r = p; // r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
- 离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。
- 声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
(2)、const限定符(常量):
- const对象一旦创建后其值就不能再改变;
- 默认状态下,const对象仅在文件内有效;
- const变量不管是声明还是定义都添加extern关键字,(只在一个文件中定义const,而在其他多个文件中声明并使用它);
(1)、const的引用:
int i = 42;
int &rl = i; //引用ri绑定对象i
const int &r2 = i; //r2也绑定对象i,但是不允许通过r2修改i的值
rl = 0; //r1并非常量,i的值修改为0
r2=0; //错误:r2是一个常量引用
(2)、指针和const:
- 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
- 所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const指针:
把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π //pip是一个指向常量对象的常量指针
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值:
*pip = 2.72; //错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr) {
errorHandler();
*curErr = 0; //正确:把 curErr所指的对象的值重置
}
(3)、顶层const:
- 顶层 const (top-level const)表示指针本身是个常量,
- 底层 const (low-level const)表示指针所指的对象是一个常量。
- 指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显.
int i = 0;
int *const pl = &i; //不能改变pl的值,这是一个顶层const
const int ci = 42; //不能改变ci的值,这是一个顶层const
const int *p2 =&ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2; //靠右的const是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的const都是底层const
(4)、constexpr和常量表达式(const expression):
- 常量表达式指值不会改变并且在编译过程就能得到计算结果的表达式
const int max_files = 20 ; //max_files是常量表达式
const int limit = max_files + l; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式
const int sz = get_size(); // sz不是常量表达式,其具体值直到运行时才能获取到
- C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + l; // mf +1是常量表达式
constexpr int sz = size (); //只有当size是一个constexpr函数时才是一条正确的声明语句
指针和constexpr
- 必须明确一点,在 constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
- p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。
const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
- 与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量:
constexpr int *np = nullptr; //np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; // i的类型是整型常量
// i和j都必须定义在函数体之外
constexpr const int *p = &i; //p 是常量指针,指向整型常量
constexpr int *p1 =&j; //p1是常量指针,指向整数j
(5)、类型别名
- 传统的方法是使用关键字typedef:
typedef double wages; // wages是double的同义词
新标准规定了一种新的方法,使用别名声明(alias declaration)using来定义类型的别名:
using sI = Sales_item; // SI是sales_item的同义词
遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const char *cstr = 0; //是对const pstring cstr的错误理解
前者声明了一个指向char的常量指针:
改写后的形式则声明了一个指向const char的指针。
auto类型
- auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
int i =0, &r = i;
auto a = r; //a是一个整数(r是i的别名,而i是一个整数)
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数( ci的顶层const 特性被忽略掉了)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
- 如果希望推断出的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; // k是整数,1是整型引用
auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针1
//错误:i的类型是int而&ci的类型是const int
auto &n = i, *p2= &ci;
decltype类型指示符:
希望从表达式的类型推断出要定义的变量的类型,decltype的作用是选择并返回操作数的数据类型.
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
(6)、自定义数据结构
- 以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。
- 类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少:
struct Sales_data {
std:: string bookNo;
unsigned units_sold = 0;
double revenue = 0.0 ;
};
编写自己的头文件:
- 为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
- 头文件通常包含那些只能被定义一次的实体,如类、const和 constexpr变量。
- 头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器
- 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。
- 预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。
- 之前已经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。
C++程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量;预处理变量有两种状态:已定义和未定义。
- #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
整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。
头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要。