语言:C++
变量和基本类型
基本内置类型
算数类型
算数类型就是我们平常使用的
int, double, char, long
等等,这里需要注意的是在平时的编程中应该如何正确选择适当的类型。
- 如何选择适当的类型
- 当明确知晓数值不可能为负数的时候,选择无符号类型
- 使用
int
执行整数运算,如果超出int
的范围,则使用long long
- 执行浮点数运算选用
double
,一般不选用其他的浮点数类型
类型转换
- 当一个算术表达式中既有无符号数又有
int
值时,int
值就会转换为无符号数 - 当从无符号数减去一个值的时候,不管这个值是不是无符号数,都必须确保结果不能是一个负数
字面值常量
一个形如42的值被称作字面值常量,这样的值一望而知。
整型和浮点型字面值
- 以0开头的整数代表八进制数,以
0x
或者0X
开头的代表十六进制数 - 十进制字面值的类型是
int
- 八进制或者十六进制的字面值一般是
int
或者unsigned int
- 浮点型字面值是
double
字符和字符串字面值
- 字符字面值为
'a'
- 字符串字面值为
"Hello world!"
布尔字面值
- 字面值为
true
或者false
变量
列表初始化(只是一个简单的介绍)
是C++11新标准的一部分
想要定义一个名为num的int变量并初始化为0,以下语句都可以做到
int num = 0;
int num = {0};
int num{0};
int num(0);
用花括号
初始化变量就是列表初始化,列表初始化有一个特点,如过我们使用列表初始化且初始值存在丢失信息的风险,则编译器会报错,而其他方式的初始化则不会。
定义于任意函数体之外的变量被初始化为0,然而一种例外情况是,定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或者以其他形式访问此类值将引发错误。
变量声明和定义的关系(只是一个简单的介绍)
-
如果想声明一个变量而非定义它,就在变量名前面添加关键字
extern
,而且不要显示的初始化变量。extern int i; //声明而非定义i int i; //定义i extern int j = 3; //定义j
-
在函数体内部,如果试图初始化一个由
extern
关键字标记的变量,将引发错误 -
变量只能被定义一次,但可以多次声明
-
变量的定义必须出现其只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝不能重复定义
标识符
- C++内置的标识符不能被用作用户自定义标识符
- 用户自定义标识符不能连续出现两个下划线
- 用户自定义标识符不能以下划线紧连大写字母开头
- 定义在函数体外的标识符不能以下划线开头
指针
nullptr
是C++11中新出现的概念,用来替代NULL
,所以之后定义指针都用nullptr
void* 指针
可用于存放任意对象的地址,但我们对该地址中存放的到底是什么类型的对象并不了解
所以void* 指针能做的事情很有限,我们不能直接操作void* 所指向的对象。
概括来说,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间所存的对象
int i = 1024, *p = &i, &r = i;
上面这条语句是正确的!
*
和&
并不是作用于本次定义的全部变量,所以不能吧int*
,int&
看成是一体的。
const限定符
-
const对象必须初始化
-
const对象仅被设定为在文件内有效,当多个文件中出现出现了同名const变量时,等于是在不同文件中分别定义了独立的变量
-
如果想在多个文件中共享const对象,必须在变量的定义之前添加
extern
关键字extern const int bufSize = fcn();//文件1中初始化了一个常量,该常量能被其他文件访问 extern const int bufSize;//与文件1中定义的bufSize是同一个
如果想要在文件中共享const变量,那么无论是声明还是定义都需要在前面加上extern
非const变量的定义是不需要加extern的
-
常量引用 reference to const
const int ci = 1024; const int &r1 = ci; //正确,用常量引用引用一个常量 r1 = 42; //错误,r1是对常量的引用 int &r2 = ci; //错误,不能用非常量引用引用一个常量对象 int i = 10; const int &ri = i; //正确,这是常量引用引用一个非量对象 const int &r = 42; //正确
-
指针和const
const double pi = 3.14159; double *ptr = π //错误 const double *ptr = π //正确 const double *const pip = π//从右往左读,左边的const是底层const,右边的const是顶层const double * p = pip; //错误,pip指向一个常量double,而p是一个非常量double指针
-
顶层const和底层const
-
顶层const作用于对象本身
int *const p = &i; //p只能指向i,不能指向别人,但是可以用*p改变i const int ci = 42; //ci只能是42,不能改变 引用本身就自带顶层const属性 //引用本来就不能指向别人
-
底层const表示指针所指或者引用的对象是个常量
const int *p2 = &ci;//p2还可以指向别人,但是不能用*p2改变ci const int &r = ci; //r既不能引用别人,也不能改变ci
-
-
constexpr和常量表达式
常量表达式:是指值不会改变并且在编译过程就能得到计算结果的表达式。显然字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
const int a = 12; //a是常量表达式 int b = 27; //b不是常量表达式 const int c = get_size();//c不是常量表达式,因为c不能在编译的时候得到值
-
constexpr
变量(比较难的概念)C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式,后面我们还会介绍constexpr函数,这种函数应该足以简单到使得编译时就可以计算其结果。
一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型
const int *p = nullptr; //p是一个指向整型常量的指针 constexpr int *q = nullptr; //q是一个指向整数的常量指针,constexpr把它所定义的对象置为了顶层const constexpr int i = 42; //i是整型常量 constexpr const int *p = &i; //p是常量指针,指向整型常量i; constexpr定义的常量必须定义在函数体之外,constexpr定义的常量指针指向的常量也必须定义在函数体之外。
-
处理类型
-
类型别名
typedef 或者C++11新增加的using
typedef char *pstring; //pstring是指向char的指针 using pstring = char*; //和上面等价 一个很重要的对比: const pstring cstr = 0;//cstr是指向char的常量指针,这里const char*是基本数据类型 const char *cstr = 0; //cstr是指向常量char的指针,这里const char是基本数据类型
-
auto
auto定义的变量必须有初始值
-
auto一般会忽略掉顶层的const,保留底层的const
const int ci = 0; //ci是整型常量,这里是顶层const auto b = ci; //b是一个整数,顶层const被忽略掉了 const auto c = ci;//c是一个整型常量
编写的自己的头文件
-
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样
-
头文件中通常包含那些只能被定义一次的实体,如类,const和constexpr变量。头文件中也时常包含其他的头文件
-
头文件中不应该有using声明
预处理器概述
预处理器是在编译之前执行的一段程序,可以部分改变我们所写的程序
#include,#ifdef,#ifndef,#endif都是预处理器
每个头文件中都应该有下面这样的结构 #ifndef SALES_DATA_H #define SALES_DATA_H //基于头文件中类的名字构建保护符的名字,以确保其唯一性,一般全部大写 #include <string> struct Sales_data{ ... } #endif
string & vector
string
一些基础的操作之前已经写过了,参见:C++中String类用法小结
输入
string s;
cin >> s;
cout << s << endl;
上面的程序在进行读取操作时,string对象会自动忽略开头的空白,并从第一个真正的字符开始读起,知道遇到下一个空白为止。比如输入是“ Hello World ”,则输出将是“Hello”。
-
getline()
getline是按行输入或者输出
string line; while(getline(cin, line)) cout << line << endl;
size()
size()的返回值是string::size_type类型的值,这是一个无符号类型的值,所以如果一个表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
字符串字面值
C++为了与C兼容,所以字符串的字面值与string是不同的类型。及单独的比如“Hello”不是string类型。
cctype头文件
改变string字符串中每个字符的特性
函数定义 | 功能简介 |
---|---|
int isalnum© | 检查字符是否是字母或数字 |
int isalpha© | 检查字符是否是字母 |
int isascii© | 检查字符是否是ASCII码 |
int iscntrl© | 检查字符是否是控制字符 |
int isdigit© | 检查字符是否是数字字符 |
int isgraph© | 检查字符是否是可打印字符 |
int islower© | 检查字符是否是小写字母 |
int isprint© | 检查字符是否是可打印字符 |
int ispunct© | 检查字符是否是标点字符 |
int isspace© | 检查字符是否是空格符 |
int isupper© | 检查字符是否是大写字母 |
int isxdigit© | 检查字符是否是十六进制数字字符 |
int toupper© | 将小写字母转换为大写字母 |
int tolower© | 将大写字母转换为小写字母 |
-
范围for语句
#include <string> #include <cctype> 使用范围for语句统计string对象中标点符号的个数 string s("Hello World!!!"); decltype(s.size()) punct_cnt; //punct_cnt的类型和s.size()的返回类型一样 for(auto c : s) if(ispunct(c)) ++punct_cnt; cout << punct_cnt << endl; 使用范围for语句改变字符串中的字符 string s("Hello World!!!"); for(auto &c : s) c = toupper(c); cout << s << endl;
下标运算符[ ]
下标运算符接收的参数是string::size_type类型
C风格字符串
使用C风格的字符串,需要包含cstring头文件
函数 | 函数介绍 |
---|---|
strlen§ | 返回p的长度,空字符串不计算在内 |
strcmp(p1, p2) | 比较p1和p2的相等性,若p1 == p2, 返回0;若p1 < p2, 返回负数 若p1 > p2, 返回正数 |
strcat(p1, p2) | 将p2附加到p1之后,返回p1 |
strcpy(p1, p2) | 将p2拷贝给p1,返回p1 |
vector
初始化
-
列表初始化(C++11新特性)
用花括号括起来0个或多个值赋给vector对象
vector<string> articles = {"a", "an", "the"}; 或者 vector<string> articles{"a", "an", "the"};
C++提供了几种不同的初始化方式,在大多是情况下可以互换使用,但也有特殊的初始化方式。
- 使用拷贝初始化时(即使用=时)
- 类内只能用拷贝初始化或者花括号的形式初始化
- 初始化列表只能把初始值放在花括号中进行初始化,而不能放在圆括号里
-
值初始化
一般情况下,可以只提供vector对象容纳的元素数量而略去初始值,此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。
vector<int> ivec(10); //十个元素都初始为0 vector<string> svec(10);//十个元素都初始为空 vector<int> ivec(10, 5);//十个元素都初始为5
如果初始化使用的是圆括号,可以说提供的值是用来构造vector对象的
如果初始化使用的是花括号,则是用列表初始化来初始化vector对象
Tips
- vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素
- 确保下标合法的一种有效手段就是尽可能使用范围for语句
迭代器(粗略讲解)
cbegin & cend
为了便于专门得到const_iterator类型返回值,C++11引入了两个新函数
auto itr = v.cbegin();//itr的类型是vector<T>::const_iterator
-
迭代器失效
但凡是使用了迭代器的循环体,都不要向迭代器所属的容器中添加元素
与旧代码的接口
混用string对象和C风格的字符串
-
允许使用以空字符串结束的字符串数组来初始化string对象或为string对象赋值
-
string对象的加法中允许使用以空字符串结束的字符串数组作为其中一个运算对象
string s(“Hello World”);//Hello World是字符串字面值,可以用来初始化string对象
```
-
但上述性质反过来不成立
char *str = s; //错误,不能用string对象初始化char* const char *str = s.c_str(); //正确
c_str()是string类专门提供的一个成员函数
使用数组初始化vector对象
-
允许使用数组初始化vector对象,只需要指明拷贝区域的首元素地址和尾后地址就可以了
int int_arr[] = {0,1,2,3,4,5}; vector<int> ivec(begin(int_arr), end(int_arr));
C++11新标准引入了两个名为begin和end的函数,将数组作为他们的参数,分别返回数组首元素的指针和尾后指针。
多维数组
使用范围for语句处理多维数组
int ia[3][4] = {{0,1,2,3},
{1,2,3,4},
{2,3,4,5}};
for(const auto &row : ia)
for(const auto col : row)
cout << col << endl;
使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型
指针和多维数组
int (*p)[4] = ia; //p指向含有四个整数的数组,p指向ia第一个内层
p = &ia[2]; //p指向ia的尾元素,p指向ia的最后一个内层
上面的做法比较复杂,可以通过auto
来简化
for(auto p = begin(ia); p != end(ia); ++p){
for(auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' ';
cout << endl;
}
类型转换
隐式转换:编译器自动进行
- 在大多数表达式中,比
int
类型小的整型值首先提升为较大的整数类型 - 在条件中,非布尔型转化为布尔型
- 初始化中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型,一般是小类型转为大类型
- 函数调用也会发生类型转换
- 一般有符号类型会转为无符号类型
int val = 3.541 + 3;
// 3转换为double型,然后与3.541相加,最后转为int型
数组转换成指针
- 在大多数数组表达式中,数组自动转换成指向数组首元素的指针
- 当数组被用作
decltype
关键字的参数,或作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生
显示转换
只介绍 const_cast
const_cast
只能改变运算对象的底层const,常常用在有函数重载的上下文中P209。
const char *pc;
char *p = const_cast<char*>(pc);
只有const_cast
可以将常量对象转换成非常量对象,即去掉某个对象的const性质,编译器就不阻止我们对该对象进行写操作了。
其他的显示转换最好不要用,所以这里也不介绍
函数
含有可变参数的函数
有时候我们无法提前预知应该向函数传递几个实参,需要用到含有可变参数的函数
- 如果所有实参类型相同,可以传递一个名为 initializer_list 的标准库类型
- 若实参类型不同,可以编写可变参数模板,这个后面介绍
initializer_list
它和 vector 一样,也是一种模板类型,所以初始化方法,拷贝,赋值,迭代器都可以类比 vector。
用如下形式编写输出错误信息的函数
void error_msg(initializer_list<string> i1)
{
for(auto beg = i1.begin(); beg != i1.end(); ++beg)
cout << *beg << " "
cout << endl;
}
在主函数中调用,expected和actual是string对象
if(expected != actual)
error_msg("functionX", expected, actual);
else
error_msg("functionX", "okay");
数组引用形参
C++允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用的形参绑定到对应的实参上,也就是绑定到数组上。
void print(int (&arr)[10]){
for(auto elem : arr)
cout << elem << endl;
}
一般来说,数组传递给形参的是一个指针,然后另外传一个参数比如数组大小来限定。
数组引用形参就不需要传另一个参数来限定范围了。
但是,数组引用形参有一个缺陷,那就是只能将函数作用于规定好大小的数组,比如上面的例子是10。
后面会介绍如何传递任意大小的数组。
列表初始化返回值
函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也可以用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化。
vector<string> process(){
// expected和actual是string对象
if(expected.empty())
return {};
else if(expected == actual)
return {"functionX", "okay"};
else
return {"functionX", expected, actual};
}
返回数组指针
声明一个返回数组指针的函数比较复杂,我们先从简单的看
使用类型别名简化
typedef int arrT[10]; // arrT是一个类型别名,它表示含有10个整数的数组
等价于
using arrT = int[10];
arrT* func(int i); // 声明了一个返回数组指针的函数
Achtung:
int arr[10];
int (*p)[10] = &arr // p是一个指针,它指向含有10个整数的数组,注意一定要加&,即使arr是一个指针
声明一个返回数组指针的函数
格式:Type(*function (parameter_list) ) [dimension]
int (*func(int i))[10];
调用 func 函数需要一个int类型的实参,对func函数的结果解引用将得到一个数组中是int的大小为10的数组
使用尾置返回类型
auto func(int i) -> int(*)[10] //这是func函数的声明
func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
使用 decltype
如果知道函数返回的指针指向哪个数组,就可以用 decltype 关键字声明返回类型
int odd[] = {1,3,5,7,9};
decltype(odd) *func(int i);
func返回一个指针,该指针所指对象与odd的类型一致
函数重载
顶层const和底层const与函数重载
- 一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开
- 一个拥有底层 const 的形参可以和另一个没有底层 const 的形参区分开
函数指针
使用函数指针
bool LC(const string &, const string &);
bool (*pf)(const string &, const string &);
// 下面两种形式等价
pf = LC;
pf = &LC
函数指针形参
// 下面两个等价
void use(const string &s1, const string &s2, bool pf(const string &, const string &)); //pf是函数类型,在形参中可以自动转化为函数指针
void use(const string &s1, const string &s2, bool (*pf)(const string &, const string &)); //这里的pf本来就是函数指针
函数指针简化
// 下面三个都是等价的函数类型
typedef bool Func(const string &, const string &);
using Func = bool(const string &, const string &);
typedef decltype(LC) Func;
// 下面三个都是等价的函数指针类型
typedef bool (*FuncP)(const string &, const string &);
using FuncP = bool(*)(const string &, const string &);
typedef decltype(LC) *FuncP;
返回函数指针
与函数指针形参不同的是,函数不能自动转换为函数指针,必须返回一个函数指针才可以
FuncP f(int); //正确
Func f(int); //错误, 应该返回函数指针
bool (*f(int))(const string &, const string &); //正确
auto f1(int) -> bool(*)(const string &, const string &); //正确
类
可变数据成员
有时我们希望能修改类的某个数据成员,即便是在一个const成员函数内。可以在在变量声明中加入mutable
关键字。
class Screen{
private:
mutable size_t access_ctr;
public:
void some_member() const{
++access_ctr;
}
};
类数据成员的初始值(C++11新特性)
Screen是一个类,作为Window类内vector的数据成员,可以初始化vector,使得Window类开始时总是拥有一个默认初始化的Screen,保存在vector的第一个元素中。
class Window{
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
提供类内初始化时,必须以符号=或者花括号表示
从 const 成员函数返回*this
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
因为成员函数后面加上const本来就是针对 this 的,表示this代表的对象是一个常量,是不允许更改的。
class Screen{
public:
const &display() const{
...
return *this;
}//不可以做左值
Screen &set(char){
...
return *this;
}
};
int main{
Screen screens;
const Screen myscreen; //只能调用display这样的常量成员函数
screens.display().set('*'); //错误,因为set会改变成员变量的值,而screen.display()函数返回值常量引用。
}
与类相关的友元
-
类不仅可以把类外函数作为友元,还可以把其他类定义为友元,也可以把其他类(之前已定义过的)的成员函数定义成友元。
-
友元关系不具有传递性。B是A的友元,并不能说明A也是B的友元。
-
若需要将其他类的成员函数定义成友元,必须明确指明该成员函数属于哪个类。
class Screen{ friend void Window::clear(); };
-
如果一个类想把一组重载函数声明为它的友元,它需要对这组函数的每一个分别进行声明。
-
在使用友元函数时,必须确保前面有该函数的类外声明。
类内元素初始化问题
-
前面讲初始化时讲过,类外元素都会有默认的初始化,一般会初始化为0,但是类内元素的初始化却不是这么简单的。主要由以下几个方法进行初始化
-
通过自己定义默认的初始化值
class example{ private: int a = 3; string str; }
在使用该类创建一个对象的时候,会调用默认的构造函数,如果类成员变量有自己定义的初始化值,默认构造时就自己定义的初始化,若是没有,则按默认的值进行初始化。比如上面 a 初始化为3,str 初始化为空字符串。
-
通过自己定义的构造函数
如果成员是const、引用、或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初始值。
能用构造函数初始值列表就尽量用,尽量不要用赋值的方式进行初始化。
-
-
在实际中,如果定义了其它构造函数,那么最好也提供一个默认构造函数。
explicit
-
抑制构造函数定义的隐式转换
string null_book = "9-999-99999-9"; // 构造一个临时的Sales_data对象 // 该对象的bookNo等于null_book item.combine(null_book);
通过在构造函数前面加上
explicit
关键字,就可以防止这种隐式转换。如果构造函数的参数大于一个,就不需要explicit
关键字了,因为这样的构造函数不存在隐式转换,只有参数等于一个的构造函数存在隐式转换。
static
- 静态成员和静态成员函数具有全局性,存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。静态成员和静态成员函数被所有相关对象共享。
- 静态成员函数只能访问静态成员,因为静态成员函数没有this指针,所以无法访问非静态成员。但是非静态成员函数是可以访问静态成员的。
- 静态成员函数可以在类内部被定义,但是静态成员应该在类外部被初始化。