C++是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度抽象和建模时,C语言则不合适。为了解决软件危急,20世纪80年代,计算机界提出了OOP(面向对象)思想,支持面向对象的程序设计语言应运而生。
本篇将为大家介绍C++的基础语法,由于C++向下兼容C语言的大多数语言特性,对于一些C语言已具备的语法,将不做论述
前文索引:
C++入门篇——深入C++基础语法(一)C++入门篇——深入C++基础语法(二)
目录
五、引用
引用的概念
引用的特性
常引用
使用场景
传值、传引用效率比较
引用和指针的区别
六、内联函数
内联函数的概念
内联函数的特性
七、auto关键字
auto的使用细则
auto不能推导的场景
八、基于范围的for循环
范围for的语法
范围for的使用条件
九、指针空值nullptr
十、extern"C"
引用,即给已经存在的变量取一个别名,与所引用的变量共用一块内存空间,而不会单独开辟新的内存空间。
使用方法:类型& 引用变量(对象名) = 引用实体;
void test()
{
int a = 1;
int& b = a;//定义引用类型,引用在语法层,没有开辟空间,而是对原空间起了新的名字
}
引用类型必须和引用实体是同种类型的
1、引用在定义时必须初始化
2、一个变量可以有多个引用
3、引用一旦引用了一个实体,就不能再引用其它实体
先来看一看下面的代码:
const int a = 1;
int& ra = a;
这样写法是否正确呢?我们知道const使得变量a具有了常属性,不能再被修改。而int&定义的引用,应该是int类型的,是可以被修改的。这样一来,只可读的变量变成了可读可写,属于权限放大,是不被允许的。
const int a = 1;
const int& ra = a;
上面的代码属于权限不变,是正确的。
那么是否存在权限缩小呢?再来看看下面的代码:
int b = 1;
const int& rb = b;
该情况属于权限缩小,也是正确的。
因此,在使用引用时,权限不能放大。
下面代码又为什么会出错呢?
double c = 1.1;
int& rc = c;
其实截断和提升时不是对本身操作,而是要通过临时变量,临时变量具有常性。rc实际上是这个临时变量的引用,而非c的引用。
因此想使上述代码正确,应改为const int& rc = c;
结论:const Type& 可以接收各种类型的对象。使用引用传参,如果函数中不改变参数的值,建议使用const Type&
int add(int& a, int& b)
{
return a + b;
}
传引用和传值可以构成函数重载,因为类型不同。但是调用时存在歧义,因为不知道调用传值还是传引用。
int& count()
{
static int n = 0;
n++;
return n;
}
传值返回返回的是变量的拷贝,若变量较小(4 or 8bit),一般寄存器充当临时变量;如果比较大,则放在调用该函数的栈帧中。
引用返回的意思是不生成变量的拷贝返回,直接返回变量的引用
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& tmp = add(1, 1);
return 0;
}
当前代码的问题:
1、存在非法访问,因为函数的返回值是c的引用,所以add函数栈帧销毁了以后,会去访问c位置的空间。
2、如果函数栈帧销毁,清理空间,那么tmp的取值就是随机值。是否清理销毁空间的值,取决于编译器的实现。
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或返回值类型非常大时,效率就更低。
引用的作用体现在传参与返回值
1、有些场景下,可提高性能(大对象+深拷贝对象)
2、输出形参数和输出形返回值(有些场景下,形参的改变可以改变实参;引用返回,可以改变返回对象)
从语法上来说,应用就是已知变量的别名,是没有独立空间的。
但从底层实现来看,引用也是依靠指针的方式实现的。想验证这一点,就要从其汇编代码来观察。
int main()
{
int a = 1;
int& ra = a;
int* pa = &a;
return 0;
}
转到反汇编:
可以看到在汇编的情况下,引用与指针的操作步骤一模一样。
对于频繁调用的小函数,能否进行优化?针对这个问题,C语言可以使用宏来解决。而C++提供了另一种方法——内联函数。
以inline修饰的函数叫内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数是可以提升程序的运行效率。
inline int add(int a, int b)
{
return a + b;
}
1、inline是一种以空间换时间的做法,省去调用函数的额外开销。所以代码很长或者有循环/递归的函数不适宜作为内联函数使用
2、inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联
3、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
C++11中,标准委员会赋予了auto全新的含义,即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto a = 1;
auto a = 'a';
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
细则:
1、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2、在同一行定义多个变量
当在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器只对第一个类型进行推导,然后用推导出来的类型定义其它变量
1、auto不能作为函数的参数
2、auto不能直接用来声明数组
3、为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4、aoto在实际中最常见的优势用法是与C++提供的新式for循环与lambda表达式等进行配合使用
范围for的使用方式如下:
#include
using namespace std;
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
for (auto e : arr)
{
cout << e << endl;
}
return 0;
}
运行过后就会依次打印该数组。
折断代码的含义是依次将数组中的每一个值赋值给e进行操作。
1、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
值得注意的是,当数组名作为函数参数,经过传参,就不能使用该方法对数组进行遍历了,因为数组名被降级成为了指针。
2、迭代的对象要实现++和==的操作
在C语言中,如果一个指针没有合法的指向,我们通常会将它赋值为NULL
NULL实际上就是一个宏:#define NULL ((void *)0)
因此在使用NULL时,会出现一些麻烦:
#include
using namespace std;
void test(int)
{
cout << "int" << endl;
}
void test(int*)
{
cout << "int*" << endl;
}
int main()
{
test(NULL);
return 0;
}
函数本意是通过NULL调用第二个test函数,但实际上,NULL被定义为0,导致匹配出错,无法正确调用函数。
针对这个问题,C++11给出指针空值nullptr
注意:
1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2、在C++11中,sizeof(nulllpt)与sizeof((void*)0)相同
3、为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
在工程中,我们可能会使用C++程序调用C的静态库,也可能使用C程序调用C++的静态库。如果我们不加改动直接调用,就会导致编译失败。这是因为C和C++函数的名字修饰不同,导致不能相互找到对应函数的地址。这时候extern"C"就排上了用场。
1、C++程序调用C库。在C++程序中加extern"C"
引头文件时:
//告诉C++编译器,大括号内的函数是用C编译器编译的,链接时要用C的函数名修饰规则去找
extern"C"
{
#include "../../test2/test.h"
}
2、C程序调用C++库。在C++库中加extern"C"
在C++程序的头文件中:
_cplusplus是C++程序会自动生成的,利用这点,我们可以使用如下写法:
#pragma once
#ifdef _cplusplus
extern"C"
{
#endif
void func(int a);
#ifdef _cplusplus
}
#endif
这段代码在.cpp文件中会展开extern"C",而在.c文件中调用该头文件时不会展开extern"C",保证代码编译不会出错