ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

1.c++入门篇

1.c++入门篇

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],14h
int 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

我们可以发现上面的汇编语言就不分变量不同;

引用和指针的不同点:

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同: 引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

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。

注意:

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

📌 相关阅读

下一篇篇:《类和对象》

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进