1、C++的引用
变量名,本身是一段内存的引用,即别名(alias)。此处引入的引用,是为己有变量起一个别名。
声明如下:
int a;
int &ra = a;
引用规则
1、引用,是一种关系型声明,而非定义。不能独立存在,必须初始化,且与原类型保持一致,且不分配内存。
2、声明关系,一经声明,不可变更。
3、可对引用,再次引用。多次引用的结果,是某一变量具有多个别名,多个别名间是平行关系。
4、辨别引用与其它,&符号前有数据类型时,是引用,其它皆为取地址或按位与。
引用的真正目的,就是取代指针传参。C++引入引用后,可以用引用解决的问题。避免用指针来解决。
引用扩展了变量的作用域,传参后,就像在本地解决问题一样。避免了传 n 级指针,解决 n-1 级指针的问题,即平级内解决问题。
引用的本质
引用的本质是 const 类型的指针,即 type * const ref
。其功能类似指针,看起来又不是指针,是对指针的封装。
探讨指针的引用与引用的指针
#include <iostream>
using namespace std;
int main()
{
int *p;
int * & rp = p; //正确,指针的引用
int & * prp =&rp; //cannot declare pointer to 'int&' 错误,引用的指针
return 0;
}
结论:指针的引用可以有,引用的指针不存在
探讨指针的指针与引用的引用
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int *p;
int **pp=&p; //可以为引用再次声明引用,但不可建立引用的引用。避免了指针调计的"失误"
int a;
int & rx = a;
int & ry = a;
int & rz = ry; //这是对引用的再次引用,可以
int && rrz = rz; //引用的引用,是不存在的,C++11 赋于它新的意义,右值引用
return 0;
}
结论:指针的指针可以有,引用的引用不存在
探讨指针数组与引用的数组
#include <iostream>
using namespace std;
int main()
{
int a, b, c;
int * pArr[] = {&a,&b,&c};
int & rArr[] = {a,b,c}; //err int & * 引用的指针不存在,这个肯定也不存在
return 0;
}
结论:指针数组有,引用数组没有
探讨数组的引用
//pr和ra都是array的引用
int main()
{
int array[5];
cout << "sizeof(array)" << sizeof(array) << endl; //20
//数组名的两重性
int * const & pr = array; //int * &,这里必须加const且只能放在这个位置
//(因为array是常量,不可以修改,所以加const修饰),不然报错
//这里array用的是数组名代表首元素的地址array为int*类型,
//这里C++中不允许pr可以改变,从而改变了数组,要写成const
cout << "sizeof(pr)" << sizeof(pr) << endl; //8
int (&ra)[5] = array; //数组的引用,array在这里是代表数组整体
cout << "sizeof(ra)" << sizeof(ra) << endl; //20
return 0;
}
C++中的const与常引用(const&)
在C++中的const才是真正的const,C语言中const修饰的变量,可以通过指针去修改,但是在C++中的const是不可以的,是真正的const。在C++中都是使用const来代替#define宏定义的。
要注意的是,C++中使用const的变量,一定要在定义的时候初始化,不然会报错。
常引用
const 的本意, 即不可修改。 所以, const 对象, 只能声明为 const 引用, 使其语义保持一致性。 非const 对象, 既可以声明为 const 引用, 也可以声明为非const 引用。声明为 const 引用, 则不可以通过 const 引用修改数据
#include <iostream>
using namespace std;
int main()
{
const int val = 10;
//int & rv = val; 这是错误的
const int & rv2 = val; //注意const的位置,可以在int前后,&之前
int data;
int &rd = data;
const int &rd2 = data;
return 0;
}
临时对象的常引用
临时对象, 通常理解为不可以取地址的对象, 比如函数的返回值, 即 CPU 中计算产生的中间变量通常称为右值。 常见临时值有常量, 表达式等。
#include <iostream>
using namespace std;
//临时变量 即不可取地址的对象
//常量 表达式 函数返回值 类型不同的变量
int foo()
{
int a = 100;
return a;
}
int main()
{
//常量
const int & cc = 55;
cout<<cc<<endl;
//表达式
int a = 3; int b = 5;
const int &ret = a+b;
//函数返回值
const int& ra = foo();
//类型不同的变量
double d = 100.12;
const int &rd = d;
return 0;
}
本质上 const 引用, 引用了一个不可改变的临时变量,const int tmp = data; c onst int &rd = tmp;
此时, 我们改变了 data 的值, 临时变量的值也并没有发生改变。
const type & (存在)与 type &const (不存在)这两个修饰方式,此处可以参考 const 修饰指针的用法, int * const p 的话, 表示 p 不可更改指向, 而引用呢, 本身一经声明, 即不可更改指向, 故也不存在这个修饰方法了。意思就是比如&ra = a;了,那么ra就一直指向a了,不可以更改的。
2、堆内存操作
C 语言中提供了 malloc 和 free 两个系统级别的函数, 完成对堆内存的申请和释放。
而 C++则提供了两关键字 new/new[]和 delete(delete[]), 此两关健字, 不仅可以提供堆内存的申请和释放, 还有其它的用途, 主要是为了 C++面向对象而扩展的功能 。
记住new/new[]和 delete(delete[])都是关键字,不是函数。
new/new[]
申请单变量空间
new 后面直接跟类型即可, 比如 new int, 申请一个 int 类型空间的大小, int int(10)后面括号里面跟的是初始化的值 。
int main()
{
int *p = new int(10); //new 相当于 int *p = (int*)malloc(sizeof(int));
*p = 100;
cout<<*p<<endl;
int **pp = new int *; // (int**)malloc(sizeof(int*))
Stu *ps = new Stu;
return 0;
}
需要注意的是,new失败的是异常。
申请一或多维数组
new 关键字, 后面跟上类型和维度, 比如申请一个 10 个 int 类型大小的数组, 即 new int[10], 后面也可以跟初始化数据。
下面分别申请了一维数组,指针数组,二维数组,三维数组的例子
int main()
{
float *p = new float [10]{1.2,3.4};// new float [10] malloc(10*sizeof(float);
//申请了float类型的数组,并初始化
char **pp = new char *[11]; //申请了char*类型的数组(指针数组)
for(int i=0; i<10; i++)
{
pp[i] = "China";
}
pp[10] = nullptr;
while(*pp)
{
cout<<*pp++<<endl;
}
int(*ppp)[5] = new int[3][5]; //申请了二维数组的空间
int (*pppp)[3][5] = new int[2][3][5]; //申请了三维数组的空间
}
释放空间 delete 和 delete[]
对于申请的单变量空间, 采用 delete 后面跟指针即可, 比如 delete p; 对于一维或是多维数组一律采用 delete[] 。
int main()
{
int *p = new int;
delete p; //释放单变量
int **pp = new int*[10];
delete []pp; //释放指针数组
int (*ppp)[5] = new int[3][5];
delete []ppp; //释放二维数组
}
对于 malloc 申请出的空间, 类型是 void*型, 如果申请失败, 则返回 NULL, 可以能过返值对申请成功与否作过判断。
new 申请空间失败不是返回 NULL,而是抛出异常, 应当采用 try catch 结构对其处理。 当然也我们可以采用不抛出异常的方式, 此时用法同 malloc。
使用规则
1 、new/delete 是关键字, 效率高于 malloc 和 free。
2 、配对使用, 避免内存泄漏和多重释放。
3 、避免, 交叉使用。 比如 malloc 申请的空间去 delete, new 出的空间被 free。
此两关键字, 重点用在类对象的申请与释放。 申请的时候会调用构造器完成初始化,释放的时候, 会调用析构器完成内存的清理。 等后面说道类的时候,再提new和delete。
3、C++的类型强制转化
使用 C 风格的强制转换可以把想要的任何东西转换成合乎心意的类型。但是新类型的强制转换可以提供更好的控制强制转换过程, 允许控制各种不同种类的强制转换。 C++提供了四种转化 static_cast, reinterpret_cast, dynamic_cast 和const_cast 以满足不同需求, C++风格的强制转换好处是, 它们能更清晰的表明它们要干什么。 C 语言转换风格, 在 C++中依然适用。
static_cast
语法格式 | static_cast<type>(expression) |
---|---|
适用场景 | 在一个方向上可以作隐式转换, 在另外一个方向上就可以作静态转换。 双向可隐式的自然不需要强制转换。 |
#include <iostream>
using namespace std;
int main()
{
//双向隐式
int a = 3;
float b = 5.5;
a = static_cast<int>(b) ;
b = static_cast<float>(a);
int x =10; int y = 3;
float z = static_cast<float>(x)/y;
cout<<z<<endl;
//单向隐式
void *p; int *q;
//q = p; //err
p = q;
q = static_cast<int*>(p);
char *pp = static_cast<char*>(malloc(100));
return 0;
}
reinterpret_cast
语法格式 | reinterpret_cast<type>(expression) |
---|---|
适用场景 | "通常为操作数的位模式提供较低层的重新解释"也就是说将数据以二 进制存在形式的重新解释, 在双方向上都不可以隐式类型转换的, 则 需要重解释类型转换。 |
int main()
{
int *p; float *q;
//p = q; //err
//q = p; //err
//p = static_cast<int*>(q); //err
//q = static_cast<int*>(p); //err
p = reinterpret_cast<int*>(q);
q = reinterpret_cast<float*>(p);
return 0;
}
const_cast
语法格式 | const_cast<type>(expression) |
---|---|
适用场景 | 用 来 移 除 非 const 对 象 的 引 用 或 指 针 的 常 量 性 (cast away the constness)使用 const_cast 去除 const 对于引用和指针限定, 通常是 为了函数能够接受参数或返回值。 非 const 对象--> const 引用或指针-->脱 const-->修改非 const 对象 |
评价 | 被 const 引用后的, 返作用。 |
注意 | 目标类类型只能是指针或引用,否则报错 |
原生数据是非 const 的, 可以去除其引用的 const 的属性, 若原生数据是 const 的,去除其引用的 const 属性, 执行任何写入操作都是未定义的。
#include <iostream>
using namespace std;
//const_cast 只作用于指针和引用,去 const 化
//const_cast 理解为, const semantic 的补充
//非 const 对象-->const 引用/指针->去 const->原非 const 对象修改
void foo(const int &a)
{
const_cast<int&>(a) = 200;
}
int main()
{
int a;
const int & ra = a;
a = 100;
cout<<a<<endl;
const_cast<int&>(ra) = 300;
cout<<ra<<endl;
cout<<a<<endl;
const int *p = &a;
*const_cast<int*>(p)= 400;
cout<<*p<<endl;
foo(a);
return 0;
}
dynamic_cast
用于多态中的父子类之间的强制转化, 以后再讲。
4、命名空间
命名空间为了大型项目开发, 而引入的一种避免命名冲突的一种机制。 比如, 在一个大型项目中, 要用到多家软件开发商提供的类库。 在事先没有约定的情况下, 两套类库可能存在同名的函数或是全局变量而产生冲突。 项目越大, 用到的类库越多, 开发人员越多,这种冲突就会越明显。鉴于这类大型项目的开发需要, C++引入的了命名空间的概念。
namespace 定义
语法 C++引入 namespace 的本质是对全局命名空间再次划分。 确切的说, 是对全局的函数和变量再次进行作用域打包。
命名空间的声明及 namespace 中可以包含的内容。
namespace spacename {
global varialbe; //全局变量
function; //函数
struct type; //结构体类型
other namespace; //其他命名空间
}
常见的命名空间有三种层次的用法, 示例如下: 假设有空间空间
namespace sn {};
方法 | 举例 | 备注 |
---|---|---|
sn::member; | sn::a = 5; sn::foo(); | 直接使用命名空间成员, 不会产生命名冲突。 |
using sn::member | using sn::a; using sn::foo(); | 命名空间成员, 融入该语句所在的命令空间, 可以会产生冲突。 |
using namespace sn; | using namesapce std; | 将命名空间(即全体成员), 融 入该语句所在的命名空间。可能会产生命令空间。 |
using namespace one::other; | using namespace one::other; | 命名空间支持嵌套。 |
标准命名空间 std::
std 是 C++标准库的命名空间,这就是在学习 C++一开始的时候, using namespace std的原故。 当然了, 前述的几种使用方法依然适用。但是, 由于标准库定义的方法, 大家多是耳熟能详, 所以一般不会跟标准库产生命名
冲突, 所以才有了这种偷懒的写法, using namespace std; 。
协同开发
相同的命名空间会自动合并,比如我在a.h中定义了namesapce one { },我又在b.h中定义了namespace one { },那么这两个命名空间会自动合并,所以我们在实际工作中,才可以协同开发,使用相同的命名空间会自动合并。.h和.cpp中都要用namespace包含起来。
5、string类
string 是 C++中处理字符串的类, 是对 C 语言中字符串的数据和行为的包装。 使对字符串的处理, 更简单易用。
使用string的时候要包含头文件#include <string>
,有时候不包含也能用是因为在iostream中包含的头文件中层层包含了string这个头文件。(我找过,找到了)
定义一个字符串的时候,这样定义即可string str = "China";
注意:在我们用cin输入string类型的时候,遇到空格之后,空格后面的东西会消失,不会算作string的内容,我们想要解决这个问题,可以使用getline(cin,s)
这里的s是string类型的。
string 重载了运算符+ < > = != += , 使字符串的操作变的非常简单。 其运算方式类似于普通数值运算, 这是运算符重载的好处。 运算符重载我们过后讲。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string s1 = "China";
string s2 = "China";
if(s1 < s2)
{
cout<<"s1 < s2"<<endl;
}
else if(s1 > s2)
{
cout<<"s1 > s2"<<endl;
}
else
{
cout<<"s1 == s2"<<endl;
}
s1 += s2;
cout<<"s1 ="<<s1<<endl;
return 0;
}
数值与字符串互转函数(全局函数,C++11新增)
value to string
string to_string(int val); |
---|
string to_string(unsigned val); |
string to_string(unsigned long val); |
string to_string(long long val); |
string to_string(unsigned long long val); |
string to_string(float val); |
string to_string(double val); |
string to_string(long double val); |
#include <iostream>
using namespace std;
int main()
{
int a = 1234;
string str = to_string(a);
str += "5678";
cout<<str<<endl;
return 0;
}
string to value
int stoi(const string& str, size_t *idx=0, int base=10); |
---|
long stol(const string& str, size_t *idx=0, int base=10); |
unsigned long stoul(const string& str, size_t *idx=0, int base=10); |
long long stoll(const string& str, size_t *idx=0, int base=10); |
unsigned long long stoull(const string& str, size_t *idx=0, int base=10); |
float stof(const string& str, size_t *idx=0); |
double stod(const string& str, size_t *idx=0); |
long double stold(const string& str, size_t *idx=0); |
#include <iostream>
using namespace std;
int main()
{
string str = "1111a";
int a = stoi(str);
a++;
cout<<a<<endl;
string strbin = "-10010110001";
string strhex= "0x7f";
int bin = stoi(strbin,nullptr,2);
int hex = stoi(strhex,nullptr,16);
cout<<"bin:"<<bin<<endl;
cout<<"hex:"<<hex<<endl;
return 0;
}
string数组
string 数组在 C 语言中要存储如下这样的数据, 要用到二维空间, 要进行两个层次的空间申请与释放但在 C++中就不需要了, 并且一样是高效的。
1 | ||||||||
---|---|---|---|---|---|---|---|---|
2 | 2 | |||||||
3 | 3 | 3 | ||||||
4 | 4 | 4 | 4 | |||||
5 | 5 | 5 | 5 | 5 | ||||
6 | 6 | 6 | 6 | 6 | 6 | |||
7 | 7 | 7 | 7 | 7 | 7 | 7 | ||
8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | |
9 | 9 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string strArray[10] = {
"0",
"1",
"22",
"333",
"4444",
"55555",
"666666",
"7777777",
"88888888",
"999999999",
};
for(int i=0; i<10; i++)
{
cout<<strArray[i]<<endl;
}
return 0;
}
Bjarne Stroustrup's Advice
(一) 在 C++中几乎不需要用宏, 用 const 或 enum 定义显式的常量, 用 inline 避免函数调用的额外开销, 用模板去刻画一族函数或类型, 用 namespace 去避免命名冲突。
(二) 不要在你需要变量之前去声明, 以保证你能立即对它进行初始化。
(三) 不要用 malloc, new 运算会做的更好。
(四) 避免使用 void*、 指针算术、 联合和强制, 大多数情况下, 强制都是设计错误的指示器。
(五) 尽量少用数组和 C 风格的字符串, 标准库中的 string 和 vector 可以简化程序。
(六) 更加重要的是, 试着将程序考虑为一组由类和对象表示的相互作用的概念, 而不是一堆数据结构和一些可以拨弄的二进制。
版权属于:孟超
本文链接:https://mengchao.xyz/index.php/archives/354/
转载时须注明出处及本声明