1.c++中的关键字
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
2.c++的输入和输出
这里就演示了一个输入和输出的过程;
在c++中,cout是标准输出对象(控制台); cin是标准输入对象,主要是从键盘读取数据。这个输入和输出必须包含头文件<iostream>, 以及命名空间;
endl是特殊的C++符号,表示换行输出
3.缺省参数;
1.概念:
缺省参数是声明和定义函数时为函数参数指定的一个缺省值。缺省值是默认值;在调用这个函数的时候,如果没有传递实参则采用形参的缺省值(默认值),如果调用的时候传递了实参:
这里我们发现缺省参数可以分为两种情况:全缺省参数和半缺省参数;
2.全缺省参数:
这里我们就可以看出来,对于缺省参数,在进行传递参数是从左到右传递的;先对函数中左边的a进行赋值,在依次对右边的参数b,c进行赋值;
3.半缺省参数
半缺省参数的意思就是不是所有参数都有一个默认的值;并且缺省的参数是从右向左进行缺省的;也就是说,如果有缺省,最右边的参数一定是有默认值的;
我们发现这里报错了,怎么回事呢?
a → 没有默认值 ← ★ 它是"必填" b → 有默认值 1 c → 有默认值 2这里的a没有默认值,所以这个a是必须传参的;最少传一个参数,不能不传;
这才是正确写法;
4.声明和定义:
在c++中,缺省参数是可以在函数声明或者函数定义中指定的,但是两者不能同时指定;否则就会编译报错;
主要原因是,两个位置可能会出现函数相同,但是默认参数的值是不同的,这种情况下,编译器就无法确定到底哪个该用缺省参数;
这里我们就可以看到报错了,说是我们重定义了默认参数;
所以,我们不能既在函数定义中指定默认参数,又在函数声明中指定默认参数,通常情况下,我们只在函数声明中指定默认参数;
【这里为什么报错呢?】
编译 main.cpp 时,编译器只看得到 a.h 里那句 "void Func(int a, int b);" 它根本不知道 a.cpp 里写了 = 20 → 它认为你必须传 2 个参数 → 报错【我们把流程展开看看:】
① 预处理:把 #include 的文件内容原样贴进来 ② 编译:每个 .cpp 独立编译,只认自己看到的声明 → 检查参数个数 ③ 链接:把各个 .o 的符号拼起来(只对名字,不检查参数默认值)我们知道,当我们做一个项目完成之后,我们向用户展示的是什么?是头文件中函数的声明,对吧?所以我们统一将默认参数写在声明里;
【正确写法】
// a.h —— 默认值写在声明里 ✓ void Func(int a, int b = 20); // a.cpp —— 定义里不再写默认值 ✓ void Func(int a, int b) { std::cout << a << " " << b; } // main.cpp #include "a.h" int main() { Func(1); // 现在编译器知道 b 默认是 20 }4.函数的重载
在c++中,函数重载就是统一作用域内,可以定义多个同名的函数;但是,参数列表根本不同(参数类型,参数数量,参数顺序不同),注意一点:参数的顺序不同,必须是参数的类型也不同才行;
注意:如果是同名函数,且参数列表相同,只有函数返回值不同,这种情况不是函数重载
#include <iostream> #include <cstdio> using namespace std; // 参数类型不同 —— 合法 int sub(int a, int b) { cout << "整型两个参数相加" << endl; return a + b; } double sub(double a, double b) { cout << "浮点型两个参数相加" << endl; return a + b; } // 参数个数不同 —— 合法 void acc() { cout << "没有参数" << endl; } void acc(int a) { cout << "只有一个参数:" << a << endl; } // 参数顺序不同 —— 必须类型也不同才合法 void acc(int a, double b) { cout << "顺序:int, double" << endl; } void acc(double a, int b) { cout << "顺序:double, int" << endl; } int main() { cout << "== 参数类型不同 ==" << endl; sub(1, 2); sub(1.1, 1.2); cout << "== 参数个数不同 ==" << endl; acc(); acc(1); cout << "== 参数顺序不同 ==" << endl; acc(1, 2.5); // int, double acc(2.5, 1); // double, int return 0; }【为什么c语言不支持函数重载?而c++支持?】
我们都知道,我们程序想运行起来需要经历:预处理,编译,汇编和链接。这四个阶段;想深入了解这四个阶段的读者,可以看《程序员的自我修养》这本书,很好;
但是c和c++子啊进行汇编的时候有一点差异:
c++在编译器进行编译的时候会对函数名进行修饰,将函数名,参数类型,返回类型等信息进行编码成一个符号名;
而c语言在编译时直接使用函数名作为符号名,不进行任何其他修饰;
5.引用
1.概念
在c++中,引用是给已存在的变量取一个别名;然后我们引用的变量和引用共用同一块空间;
格式:类型& 引用名 = 变量名;
注意,看上面的代码,我们是对int类型的变量进行引用,则他的引用类型是:int&;
引用是c++特有的;
2.引用的特征;
【1.引用在定义的时候必须要进行初始化】
引用定义的时候不初始化会报错:
这里就报错了;
【2.一个变量可以多次引用】
这也就说明了引用和引用的参数是用的同一块空间;改变任何一个引用的值,其他值都会改变:
【3.一单引用了一个变量,就不能引用其他变量了;】
我们发现没,其实这里只是将p的值,赋值给了b?
3.引用的使用场景;
引用可以做函数参数和函数返回值;
【1.做函数的参数】
引用作参数可以:
1.减少实参和形参之间的拷贝,提高效率;
2.做输出型参数
我们使用引用传参,可以直接操作实参变量,而不是进行实参和形参之间的拷贝,这样一来减少了拷贝,就提高了效率;
我们这里不仅没有开辟新的空间,也没有进行拷贝,使我们同时提高了时间和空间的效率;
【2.做输出型参数】
使用指针完成的参数传递;
我们使用引用看看:
【3.做函数返回值】
我们都知道我们在调用一个函数进行返回的时候,再没有引用的情况下,通常会先将这个函数的返回值放到一块单独的临时空间中,这个空间通常是一个寄存器;但是如果这个返回值是引用,我们就不需要设置这个临时空间了;
看上面这块代码,可能有读者会问,这里为什么在变量前面加上static?我们知道正常情况下,函数在结束销毁之后仍然存在的变量,如静态变量,动态申请的变量等;因为如果不是这样的变量(我们的返回值还是引用,不存在临时存储),那么在函数调用结束后函数会进行销毁,此时,函数中的一般变量都会被销毁,那么返回值就是随机值了;
我们详细看看:
【按值返回】
int Func() { int n = 10; return n; // 把 n 的值"复制"出去 } // n 在这里就死了这里即使函数进行了销毁,也无所谓啊。因为,我们的返回值已经存到临时空间中去了;
【返回引用】
int& Func() { int n = 10; return n; // ❌ 把 n 的地址交出去 } // n 死了,那块栈内存被回收 int& r = Func(); cout << r; // 读到的是垃圾(未定义行为)这里当n被销毁以后,调用者拿到的是垃圾;
【加上static之后呢?】
int& Func() { static int n = 10; // ✅ 存在静态存储区,不在栈上 return n; }static局部变量的特点
① 存放在静态数据区(不在栈帧里) ② 函数返回时不会被销毁 ③ 生命周期 = 整个程序运行期间 ④ 只初始化一次4.常引用
常引用是一种对引用对象的限制;其中涉及一些对权限的放大和缩小;
看到这里的报错了吧,也就是说,我们的权限是可以缩小,但是不能放大;也就是说:对于const修饰的变量,无法对齐进行引用;
但是如果是这样写,就没什么毛病了;
5.引用和指针的区别;
在语法概念上引用就是一个别名,没有独立的空间和其引用实体共用同一块空间;
int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; r在底层实现上实际是有空间的,因为引用是按照指针的方式来实现的;
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }我们对比一下汇编语言:
int a = 10; mov dword ptr [a],0Ah int& ra = a; lea eax,[a] mov dword ptr [ra],eax ra = 20; mov eax,dword ptr [ra] mov dword ptr [eax],14hint a = 10; mov dword ptr [a],0Ah int* pa = &a; lea eax,[a] mov dword ptr [pa],eax *pa = 20; mov eax,dword ptr [pa] mov dword ptr [eax],14h我们可以发现上面的汇编语言就不分变量不同;
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同: 引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
6.内联函数
1.概念
格式:inline + 函数;
内联函数在被调用的时候会直接在调用的地方展开,这样一来,内联函数就没有函数的调用栈帧的开销,从而大大的提高了效率;
2.特性
【1】内联函数是一种使用空间换时间的做法,如果编译器将函数当做内联函数进行处理,在编译阶段,会用内联函数的函数体直接替换函数的调用;虽然提高了运行效率。但是会导致目标文件变大;
【2】内联函数对于编译器来说只是一个建议,不同编译器内联函数的实现机制可能会相同,正常情况下,将函数规模较小、不是递归、且调用频繁的函数采用inline修饰,否则编译器可能会忽略inline的特性;
【3】inline不能像普通函数那样分离声明和定义,分离会导致链接错误,因为编译器认为内联函数在进行调用的时候会被展开,他是不会进符号表的,也就没有函数的地址,链接的时候找不到,编译器在遇到内联函数的时候必须能够看到函数的完整定义,否则无法进行展开;
7.auto关键字;
1.简介
auto关键字是用于自动推导类型,他让编译器根据初始化表达是自动推断变量的类型,常常用于简化复杂类型;
2.使用准则
【1】在我们使用auto关键字定义变量的时候必须进行初始化,否则即会报错;
因为,编译器在编译阶段的时候会根据初始化的表达式来推导auto的实际类型,因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在进行编译的时候会将auto替换为他推导的变量类型;
【2】
用auto声明指针变量的时候使用auto和auto*是没有任何区别的;因为不管怎么样auto基本都能识别出来;但是用auto声明引用类型时则必须加&;
【3】在同一行声明多个变量时候,这些变量必须是同一类型;因为编译器只会对第一个类型进行推导,然后用推导出来的类型来进行定义其他变量,从而导致所有变量是同一类型,如果表达式的类型不同,编译器就无法确定是同一类型,从而报错;
3.不能进行推导的情况;
auto 是不能作为形参类型,因为编译器无法对a的实际类型进行推导啊;
auto也不能直接用来声明数组;
虽然,auto无法直接用来声明数组,但是可以推导数组;
这里就完成了;
8.基于范围的for循环
基本语法:
c++中引入了一种新的for循环,for循环后面的括号被冒号分为了两部分;第一部分是范围内用于迭代的变量,第二部分是表示被迭代的范围;
上述代码就是遍历数组arr,自动进行迭代,迭代完之后结束
这里的auto e:是auto让编译器自动推导的e的类型,这里推导出来的类型是int
注意:这里的for循环,和我们之前的普通循环是一样的,continue使用来结束本次循环,break是用来结束循环的;
这里的迭代对象会实现==和++的操作;
9.指针空值nullptr
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr() { int* p1 = NULL; int* p2 = 0; // ...... }NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(0); f(NULL); f((int*)NULL); return 0; }程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
📌 相关阅读
下一篇篇:《类和对象》