【C++】C++入门-1

简介: 【C++】C++入门-1

4.常引用(带有const的引用)

4.1 指针/引用在赋值中,权限可以缩小,但是不能放大

权限的缩小和放大,针对的是从引用实体到引用变量的过程中,权限的变化

int main()
{
  int a = 0;
  int& ra = a;//ra既可以读到a,也可以修改a,权限的平移
  const int& rra = a;//rra只能读到a,并不可以修改a,这里是权限的缩小
  rra++;//rra没有修改a的权限,因为他是const引用
  a++;//a本身是int修饰,没有const,可以修改
  const int b = 1;//变量b只能被读取,不能被修改
  int& rb = b;//rb没有const修饰,可以读写b,这就是典型的权限放大,编译器会报错
  int& rd = 10;//常量不可以被修改,典型的权限放大。
  const int& rb = b;//rb有了const修饰,只能读b,不能写b,权限的平移
  return 0;
}

4.2 常引用做参数

a.一般引用做参数都是用常引用,也就是const+引用,如果不用const会有可能产生权限放大的问题,而常引用既可以接收只读的权限,又可以接收可读可写的权限。

b.常引用做参数并不是为了修改参数,而是为了减少拷贝提高效率。


4.3 缺省参数如何引用?

缺省参数如果想做为引用的话,必须用常引用,因为缺省参数是一个常量,是不允许被修改的,只可以读。

void func(const int& N = 10)
{
}


4.4 临时变量具有常性不能修改(传值返回,隐式/强制类型转换)

a.常引用接收传值返回

传值返回我们前面就提到过,他返回时需要依靠一个临时变量,而临时变量具有常性不能修改,所以如果想要用引用接收那就必须用常引用,必须带上const。

int Count()
{
  static int n = 0;
  n++;
  // ...
  return n;
}
int main()
{
  int& ret = Count();
  const int& ret = Count();
}


b.常引用接收临时变量

int main()
{
  const int& b = 10;
  double d = 12.34;
  cout << (int)d << endl;
  //强制类型转换,并不是改变了变量d,而是产生临时变量,输出的值也是临时变量的值。
  int i = d;
  //隐式类型转换,也是产生了临时变量。
  const int& ri = d;//这里引用的实体其实就是从double d 到int类型转换中间的临时变量
  cout << ri << endl;//这里输出的引用实际上就是double到int中间的临时变量的别名。
  return 0;
}


5.引用和指针的区别

a.语法概念上引用变量就是一个别名,不开空间,和引用实体共用一个空间。
底层实现上引用变量其实是要开空间的,因为引用在底层上是按照指针来实现的


f3207251038945bfab520b00e584eee5.png


b. 引用概念上定义一个变量的别名,指针存储一个变量地址。


c. 引用在定义时必须初始化,指针没有要求


d. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体


e. 没有NULL引用,但有NULL指针


f. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)


g. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小


h. 有多级指针,但是没有多级引用


i. 访问实体方式不同,指针需要显式解引用,引用直接使用就好,具体细节编译器会自动处理


j. 引用比指针使用起来相对更安全



六、内联函数(不建立函数栈帧的函数,已经不是正常的函数了)

1.替代C语言中的宏


C语言中的宏在书写时,由于宏是单纯的替换,所以导致很容易出问题,例如下面,我们写一个实现两数之和的宏,大概能写出4种形式,可是这四种形式都是错的。

因为在不同的使用宏的场景下,对于宏的书写要求都是很高的。


a. 如果加分号,那么在分支语句的判断部分,会出语法错误。


b. 如果不加外层括号,可能由于运算符优先级的问题,无法得到我们想要的答案。


c. 如果内层不加括号,仅仅是加减这样的符号,都要比位操作符优先级高,这时候也无法得到我们想要的答案。


这时候,在C++中就提出了内联函数,内联函数在 ( 编译 ) 期间,编译器会用函数体来替换内联函数的调用,而不是宏那样的单纯替换

#define ADD(x,y) x+y
#define ADD(x,y) (x+y)
#define ADD(x,y) (x)+(y)
#define ADD(x,y) ((x)+(y));
int main()
{
  //不能加分号
  if (ADD(1, 2))
  {
  }
  //外层括号
  ADD(1, 2) * 3;
  //内层括号
  int a = 1, b = 2;
  ADD(a | b, a & b);//+运算符优先级高于|&
}


2.编译器根据函数体大小来决定是否展开(代码膨胀)

内联函数一般适用于频繁调用的小函数。


如果不是内联函数还频繁调用的话,就会频繁的开辟函数栈帧,这会对程序产生不小的开销,影响程序运行时的效率,内联函数不害怕这一点,因为它根本就不建立函数栈帧

同时如果内联函数体过大,编译器也会将主动权掌握在自己手里,他会决定是否在内联函数调用的地方展开函数体。

如果函数体过大,将不会展开,如果较小,就会展开,这个结论我们可以通过汇编指令来查看。

inline int Add(int x, int y)//频繁调用的小函数,推荐搞成内联函数。
{
  return x + y;
}
inline int func(int x, int y)//编译期间不会展开
{
  int ret = x + y;
  ret = x + y;
  ret += x + y;
  ret = x * y;
  ret = x + y;
  ret *= x - y;
  ret = x + y;
  ret = x / y;
  ret += x + y;
  ret /= x + y;
  ret *= x + y;
  ret = x + y;
  return ret;
}
int main()
{
  int ret = Add(1, 3);
  int ret2 = func(1, 2);
  return 0;
}


由于debug版本下我们要对代码进行调试,所以代码中不会展开内联函数体,我们需要先将工程属性设置成这样子,然后打开调试中的反汇编查看底层的汇编指令,看看编译器对于内联函数体展开的情况。

0254e54371794cffb3c9e2e3db8b68d3.png


下面的汇编指令就可以验证我们之前的结论,内联函数体过大,编译器不展开内联函数调用的地方,函数体较小的时候编译器会在内联函数调用的地方展开。

5f63cff53e0b49c88a583518ca5e00ab.png


函数体较长时,编译器不会展开是因为代码膨胀,假设函数体中的指令有30行,程序中内联函数调用的地方有10000处,一旦编译器展开函数体,程序就会瞬间出现30w行指令,这会疯狂增加可执行程序文件的大小,也就是安装包的大小,所以编译器不会让这样的事情发生,即使你对编译器发出了内联的请求,编译器也不会管你,说了句 ‘’ 莫挨劳资,走远点 ‘’


3.声明和定义分离(本质:内联函数无论是否被编译器当作内联处理,他的函数名和有效地址都不进符号表,与static修饰的全局函数一样,都不进符号表)


如果下面这部分知识不太清楚的话,可以看看下面这篇博文,补一下基础,因为接下来讲的东西需要用到下面的知识。


程序运行原理和预编译


如果内联函数的声明和定义分开的话,程序就会报链接错误,为什么呢?我们前面说过内联函数只是有可能将函数体展开,并不会建立函数栈帧,所以stack.obj文件的符号表就不会存放Add函数和它的地址,那在链接阶段,test.obj会根据Add的函数名字到stack.obj文件的符号表中寻找Add函数的有效地址,但可惜符号表中别说地址了,连函数名都没有,自然目标文件之间的链接就无法成功,编译器就无法识别test.cpp中的Add到底是什么,光有个函数声明,没有函数定义编译器也就会报错:无法解析的外部符号。

ef840d50b0884412a1c20e9de4487bfe.png


结论:内联函数在定义时不要搞到.c文件里定义了,直接在.h文件里面定义就好,不要把定义和声明分开,这样在展开.h文件之后,函数体就在那里,链接阶段就不会在去找函数的地址了,因为函数就在他自身的目标文件里面。



七、auto用法


1.补一下C语言芝士

第一行const直接修饰的是指针变量p1,所以指针变量p1本身不能修改,它指向的内容还是可以修改的,但p1现在被你搞成const修饰了,所以它必须被初始化,因为它只有一次赋值的机会,就是在初始化的那个地方,不能说你后面在去给p1赋值,这样不可以。

第二行const修饰的不是二级指针p2,修饰的是二级指针p2所指向的内容,那么指针变量p2是没有被const修饰的,所以p2可以不初始化,但p2所指向的内容是不可以发生改变的,因为const修饰的是p2指向的内容。


注意:语法检查的时候,是不会先替换typedef内容的,他会先直接分析你的代码是否在语法上存在问题,比如第一行代码,编译器是不会把pstring替换为char的,如果替换为char当然这句语句就没有问题了,不初始化也OK,但是编译器看的不是替换之后的,他在预编译之前就发现你这段代码语法有问题,所以编译器就直接会报错了,因为他认为p1就是个变量,你用const修饰了,那就必须给初始值,第二行代码编译器认为p2是个指针,因为它看到*的存在了,所以它认为const修饰的是p2指向的内容,不是p2本身


出现分析问题错误的原因,其实就是我们思考的是替换之后的结果,编译器在分析语法时,只会看到代码本身,根本不存在替换不替换这么一说。

typedef char* pstring;
int main()
{
   const pstring p1;    // 编译成功还是失败?
   const pstring* p2;   // 编译成功还是失败?
   pstring* const p2;//如果这样写,const修饰的才是p2指针变量本身
   return 0;
}


2. auto用于自动推导类型

a8633cf14fa844c983f156a3dd9c50ac.png


3.auto利用逗号运算符,一行定义多个变量时,这些变量必须是相同的类型。

因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型来定义其他变量,所以你定义的多个变量就必须是同一类型的。

void TestAuto()
{
    auto a = 1, b = 2; //必须是相同的类型
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}


4.auto在推导类型时,如果想推导出引用类型则必须在auto后面加&,在推导指针类型时,auto后面加不加*都可以

int main()
{
    int x = 10;
    auto a = &x;
    auto* b = &x;//加不加*无所谓
    auto& c = x;//必须加&
    cout << typeid(a).name() << endl;//typeid().name()可以拿到类型的字符串
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    return 0;
}



5. auto不能作为函数参数,因为无法事先确定需要开辟函数栈帧的大小

void TestAuto(auto a)//编译器无法推导a的类型,开辟栈帧时也就不知道开多大。
{}



6. auto不能用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};//这是错误的声明方式
}


八、基于范围的for循环


a. C++11中引入了基于范围的for循环,for后面的括号中有两部分组成,第一部分是在范围内用于迭代的变量,第二部分表示迭代的范围。
注意:for循环与普通循环类似,既可以用continue来结束本次循环,也可以用break来跳出整个循环。

void TestFor()
{
  int array[] = { 1, 2, 3, 4, 5 };
  for(auto& e : array)//将迭代变量搞成引用,这样可以直接操作数组中的数据。
       e *= 2;
  for(auto e : array)
       cout << e << " ";
  return 0;
}



b. for循环迭代的范围必须是确定的。
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

//以下代码是有问题的,因为for的范围是不确定的。
void func(int array[])//传过来的array不是数组,而是指针。
{
    for(auto& e : array)
        cout<< e <<endl; 
}



九、指针空值nullptr ==> (void*)0


在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

下面是stddef.h头文件的部分源码,所以C++98对于指针空值是没有确定的值的。

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif


C++11为了避免这样的情况发生,定义了关键字nullptr来表示指针空值,弥补C++98中有关NULL空指针的bug。

void f(int)
{
  cout << "1" << endl;
}
void f(int*)
{
  cout << "2" << endl;
}
int main()
{
  f(0);
  f(NULL);//这里原本想调用输出2的结果,但NULL被编译器默认为0,就调用了输出为1的函数,所以我们要想调用输出2的函数,就用nullptr关键字。
  f(nullptr);
  //nullptr就是(void*)0
  return 0;
}



b9869db7909b42b6b7caffbb657a9225.png



  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void * )0)所占的字节数相同
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。



































相关文章
|
1天前
|
编译器 C语言 C++
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
9 0
|
1天前
|
安全 编译器 程序员
【C++入门】内联函数、auto与基于范围的for循环
【C++入门】内联函数、auto与基于范围的for循环
|
1天前
|
存储 安全 编译器
【C++入门】缺省参数、函数重载与引用(下)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
编译器 C语言 C++
【C++入门】缺省参数、函数重载与引用(上)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
C语言 C++
【C++入门】关键字、命名空间以及输入输出
【C++入门】关键字、命名空间以及输入输出
|
1天前
|
人工智能 分布式计算 Java
【C++入门】初识C++
【C++入门】初识C++
|
1天前
|
C++ Python
C++教学——从入门到精通 10.循环
学习编程建议先Python后C++,以避免C++思维影响。课程涵盖for、while和do while循环。for循环示例:`for(int i=0;i&lt;n;i++)`,用于计算114514天后的金币总数(1145140个)。死循环通过`for(int i=0;;i++)`实现,用`break`退出。while循环格式`while(条件)`,同样可解决金币问题。do while循环特点是先执行后判断,结构为`do{...}while(条件)`。
7 2
|
2天前
|
C++
C++入门项目——通讯管理系统
C++入门项目——通讯管理系统
|
2天前
|
存储 编译器 C++
C++基础入门(超详细)
C++基础入门(超详细)
|
6天前
|
设计模式 存储 算法
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范