C++ 初探:不要‘下次一定’,立即开始你的C++之旅

闲晨 2024-08-07 12:05:02 阅读 63

C++初识

文章目录

C++ hello worldnamespace,命名空间命名空间的使用域作用限定符展开命名空间指定展开命名空间成员

C++的域

C++的输入和输出缺省参数函数重载引用(reference)引用概念引用的特性引用的使用const引用

inlinenullptr

C++ hello world

<code>#include <iostream>

//test.cpp

using namespace std;

int main()

{

std::cout << "hello world" << std::endl;

cout << "haha" << endl;

return 0;

}

使用using将命名空间展开,使用cout标准输出流,和流插入运算符 << 将hello world输出到控制台。使用endl来完成C语言的换行。C++是在C语言之上发展出来的,在C++的编译器里是兼容C语言的,两者可以混合使用。

namespace,命名空间

在C/C++中,变量、函数、类大量存在,由于它们的大量存在而可能导致名称使用上的冲突。namespace关键字,是针对名称冲突而出现的。

namespace关键字后面跟上命名空间的名字,通过大花括号( {} )括在一起,{}中为名称空间的成员,在名称空间内可以对,变量、函数、结构体、枚举等进行重命名。

#include <iostream>

namespace hx

{

int rand = 6;

int Add(int left, int right)

{

return left + right;

}

struct Node

{

int val;

struct Node* next;

};

}

namespace本质上是定义了一个域,这个域跟全局域各自独立,不同域可以定义同名变量。

想要使用名称空间的成员,必须在变量/函数/结构体/……前加上名称空间的名字和 ::域作用限定符。否则默认使用,其它地方出现的变量/函数/结构体/……

名称空间只能在全局定义,可以嵌套使用

项目工程里,多文件定义同名namespace会认为是同一个namespace,不会发生冲突

C++标准库都放在一个std的命名空间中

案例1:

#include <iostream>

namespace hx//命名空间的名字

{

int range = 6;

void swap(int x, int y)

{

std::cout << "void swap(int x, int y)" << std::endl;

}

struct Node

{

int val;

struct Node* next;

};

}

void swap(int& x, int& y)

{

int temp = x;

x = y;

y = temp;

}

int main()

{

int a = 5;

int b = 10;

int range = 1;

printf("%d %d\n", range, hx::range);

swap(a, b);

printf("%d %d\n", a, b);

hx::swap(a, b);

return 0;

}

在这里插入图片描述

案例2(嵌套定义):

<code>namespace xyz

{

namespace xy

{

int range = 6;

void swap(int x, int y)

{

std::cout << "void swap(int x, int y)" << std::endl;

}

}

namespace yz

{

int range = 3;

void swap()

{

std::cout << "void swap()" << std::endl;

}

}

}

int main()

{

int range = 66;

std::cout << xyz::xy::range << std::endl;

std::cout << xyz::yz::range << std::endl;

}

在这里插入图片描述

命名空间的使用

域作用限定符

<code>::,域作用限定符。

::变量名,访问全局变量。

命名空间名::变量名,访问了命名空间域。

struct hx::Node node;,访问结构体。

#include <iostream>

using namespace std;

namespace hx

{

int rand = 0;

void Print()

{

cout << "哈哈哈" << endl;

}

struct Node

{

int val;

struct Node* next;

};

}

int rand = 10;

int main()

{

int rand = 10;

cout << ::rand << endl;

cout << rand << endl;

hx::Print();

struct hx::Node node;

return 0;

}

展开命名空间

使用using将名称空间全部成员展开,在项目中不推荐,名称之间的冲突很大。日常做练习,测试方便可以使用,但不能养成创建一个main函数前,第一件事将,std命名空间展开,以及忘记对std命名空间的成员前使用域作用限定符的坏习惯

使用using展开前,编译器是先查找局部,然后查找全局,有了using将命名空间展开后还会到命名空间内查找,有一种将命名空间成员,改变为全局变量的意味。

#include <iostream>

using namespace std;

int main()

{

int a = 0;

cin >> a;

cout << a << endl;

return 0;

}

使用using展开后,就不需要频繁使用 std::,方便了许多。

指定展开命名空间成员

项目中经常使用,且不会存在冲突的名称

#include <iostream>

namespace hx

{

int n = 10;

int num = 20;

}

using hx::num;

int main()

{

cout << num << endl;

return 0;

}

C++的域

C++中域有函数局部域、全局域、命名空间域、类域,域影响的是变量/函数/类型的出处的逻辑, 有了域隔离(不同的域可以同名,同一个域不可以定义同一个东西),名字冲突的问题就解决了。

局部域和全局域除了会影响编译时的查找逻辑(先局部查找,然后全局查找),还会影响变量的生命周期。

局部变量存储在栈上,生命周期时临时的,结束函数的调用或作用域结束时消亡。

全局变量存储静态存储区上,生命周期是永久的,从程序开始执行诞生,到程序终止时消亡。

命名空间域和类域不影响变量生命周期。

命名空间域,只能定义在全局,它的成员本质上是全局变量。

命名空间域,与全局进行隔离

编译时想要使用变量/函数/结构体/……编译时向上查找过程必须包含对应的代码,先局部查找,然后全局查找。

C++的输入和输出

< iostream >,是Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。

std::cin,是 istream 类的对象,主要面向窄字符的标准输出,与C语言的不同,它不需要使用占位符,可以自动识别任意类型的变量。使用流提取运算符(>>)cin >> 变量名 << endl

std::cout,是ostream类的对象,主要面向窄字符的标准输出流,不需要使用占位符,%d、%s、%c这些东西,可以自动识别任意类型的变量、数据类型进行输出,使用流插入运算符(<<)

不管是什么类型的数据,都会被转换为字符流,进行输入输出

std::endl(end line),是一个函数,流插入、输出时,相当于插入一个换行符 加 刷新缓冲区。

‘\n’,这种换行方法,在不同的系统上可能不兼容,无法通过换行符进行换行,使用endl更加多用。

使用C++输入输出更加方便,不需要像printf、scanf输入输出时那样,需要使用占位符手动指定格式,C++的输入输出可以自动的识别变量的类型。

C++兼容C语言,头文件< iostream >,间接包含了printf、scanf,在vs编译器上是这样,其它编译器不一定能支持。

使用cout进行输出时,输出内容不会第一时间输出,而是先将这些东西装换为字符串,然后放在cout的缓冲区,需要遇见一些刷新标志才会出缓冲区,比如换行、flush(cout的主动刷新接口)才会输出到控制台中。

由于这些机制导致了cout的效率不会很高

这三句代码可以提高,C++IO流的效率

int main()

{

ios_base::sync_with_stdio(false);//使C语言和C++不同步兼容

cin.tie(nullptr);

cout.tie(nullptr);//将cin和cout相互绑定的关系,不会与其它流相互关联

return 0;

}

缺省参数

C++中的缺省参数(默认参数),允许在对函数进行声明或定义时为函数参数提供一个缺省值。在调用该函数时,如果没有传递参数,则使用该形参对应的缺省值,否则使用传递过来的参数,缺省参数分为全缺省和半缺省

函数声明和定义分离时,缺省参数不能再函数声明和定义中同时出现,规定必须哈数声明给缺省值,例如,实现数据结构队列的.h头文件的函数声明和.c文件对函数的定义。

全缺省参数,给全部形参缺省值,C++规定必须从左到右依次给实参,不能跳跃给实参

#include <iostream>

using namespace std;

int Add(int a = 1, int b = 2, int c = 3)

{

return a + b + c;

}

int main()

{

cout << Add() << endl;

cout << Add(10) << endl;

cout << Add(10, 20) << endl;

cout << Add(10, 20, 30) << endl;

return 0;

}

在这里插入图片描述

全缺省参数的,几种调用形式

<code>Add(),没有传递参数,调用函数add是使用三个缺省值。Add(10),传递了第一个参数,没有后续两个参数,形参b和c使用缺省值。Add(10, 20),传递了第一个和第二个参数,没有最后一个参数,形参c使用缺省值。Add(10, 20, 30),三个参数都传递了,不会使用缺省值。


半缺省参数,部分形参给缺省值,C++规定半缺省值必须从右向左依次给缺省值

#include <iostream>

using namespace std;

int Add(int a, int b, int c = 3)

{

return a + b + c;

}

int main()

{

cout << Add(10, 20, 30) << endl;

return 0;

}


例如,在对栈的数据结构进行初始化时,可以给一个缺省值,我可以不传递参数让其默认开辟4个空间。

//初始化

void StackInit(Stack* ps, int n = 4)

{

assert(ps);

ps->arr = (StackDatdType*)malloc(sizeof(StackDatdType) * n);

ps->capacity = ps->top = 0;

}

函数重载

C++中允许在同一个作用域里定义多个同名函数,只要它们的参数列表不同。这种特性使得同一个函数名可以执行不同的操作,C语言不支持这种操作。

参数列表不同返回类型可以相同,也可以不同,只凭借返回类型无法实现函数重载作用域必须在同一个

#include <iostream>

int Add(int left, int right)

{

return left + right;

}

double Add(double left, double right)

{

return left + right;

}

int main()

{

Add(1, 2);

Add(1.2, 5.2);

return 0;

}

这里实际上调用了两个函数


这里同时使用了函数重载和缺省参数,语法上没问题,当直接调用 test(),编译器直接傻眼了不知道到的用那个函数,有歧义。

void test()

{

cout << "void test()" << endl;

}

void test(int n = 4)

{

cout << "void test(int n = 4)" << endl;

}

int main()

{

test();

return 0;

}

在这里插入图片描述

引用(reference)

引用概念

很牛的东西。

引用就是取别名,引用不是新定义一个变量,而是给以存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和引用的变量共同使用同一块内存空间。比如土豆、洋芋、马铃薯,不同的称呼,指向的都是同一个东西。

C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<<和>>,这⾥引⽤也和取 地址使⽤了同⼀个符号&。

在这里插入图片描述


类型& 引用别名 = 引用对象

<code>int main()

{

int a = 6;

int& b = a;

int& c = a;

int& d = b;//也可以对别名b取别名,d也是指向变量a

d++;//对d加加,a、b、c都会加1,它们指向同一块内存。

}

int main()

{

int a = 6;

int& b = a;

int& c = a;

int& d = b;

cout << &a << endl;

cout << &b << endl;

cout << &c << endl;

cout << &d << endl;

d++;

cout << a << endl;

}

在这里插入图片描述

引用的特性

引用在定义时必须初始化

在这里插入图片描述

一个变量可以有多个引用(上面提过例子)

引用一旦引用一个实体,就不能引用其它实体,这里使用ra对变量a引用之后,还使用b进行引用,编译代码时没有报错,但运行后会报错,提示ra多次初始化

<code>int main()

{

int a = 1;

int b = 10;

int& ra = a;

int& ra = b;

//这里并非是是引用,而是一个赋值,将10赋给ra

ra = b;

return 0;

}

在这里插入图片描述

引用的使用

引用在实践中主要时用于传参和引用做返回值中,减少拷贝提高效率和改变引用对象时同时改变被用于对象

例如,我传递100w个大小的整形数组,来解决某问题,传递数组时需要临时开辟100w个大小的整形空间,非常浪费,这里对数组使用引用就可以进行优化,从而提高效率。指针也能达到这种效果。

对函数使用引用本质上是在传地址

引用传参跟指针传参相对比较复杂,指针也也有类似场景。

<code>int& StackTop(Stack& ps)

{

assert(ps.top > 0);

return ps.arr[ps.top - 1];

}

int main()

{

Stack sk;

StackInit(&sk);

StackPush(&sk, 1);

StackPush(&sk, 2);

StackTop(sk)++;

return 0;

}

这里将栈顶元素放回后++,编译器会报错。调用函数返回后,它的返回值存放在一块临时对象中它具有常性无法被修改。

在这里插入图片描述

解决办法:将返回值进行引用,这里返回的就不会将返回值拷贝在临时对象中,而是给这个返回值取了一个别名返回的是栈顶元素的别名,对别名加加的结果是栈顶元素大小加1。

在这里插入图片描述

在这里插入图片描述

<code>int& StackTop(Stack& ps)

{

assert(ps.top > 0);

return ps.arr[ps.top - 1];

}

int main()

{

Stack sk;

StackInit(&sk);

StackPush(&sk, 1);

StackPush(&sk, 2);

StackTop(sk)++;

return 0;

}

无法使用引用进行返回的场景

//指针

int* test()

{

int ret = 10;

//……

return &ret;//返回的是局部变量,函数调用完成后被销毁,返回地址的是一个野指针。

}

//引用

int& test()

{

int ret = 10;

//……

return ret;//引用同理,引用一块被销毁的空间

}

int main()

{

test()++;

return 0;

}

在这里插入图片描述

引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代最大的点,C++引用定义后不能改变指向

<code>#include <iostream>

using namespace std;

void Swap(int& x, int& y)

{

int temp = x;

x = y;

y = x;

}

int main()

{

int a = 10;

int b = 50;

Swap(a, b);

cout << a << endl << b;

return 0;

}

const引用

可以用于const对象,但是必须使用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但不能放大。指针也存在权限放大缩小。

int main()

{

const int a = 0;

//编译器报错 error C2440: “初始化”: 无法从“const int”转换为“int &”

//int& ra = a;//权限放大

const int& ra = a;//平等关系

int b = 0;

int& rc = b;

const int& rd = b;//权限缩小

//error C3892: “rd”: 不能给常量赋值

//rd++;

rc++;

return 0;

}

类似于对 int& rb = b * 2; float& d = 3.14; int& rd = d;。进行引用。对 b * 2的结果会存放在一个临时变量中,将b * 2的结果计算出来后,编译器放在一个临时对象中,存储中间值。int& rd = d;对一个浮点数进行引用,先对变量d进行类型转换,这个类型转换的结果也会被存放在临时对象中。C++规定临时对象具有常性(常性,形如被const修饰),这里触发了权限放大,必须使用常引用才可行。

所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。

const修饰的引用作为函数参数,可以引用临时对象、普通对象、const修饰的对象。

void fun(const int& ra);

inline

使用inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开内联函数,这种调用内联函数的方式就不需要要创建函数栈帧,提高了效率。

inline只是一种建议,是否展开还得看编译器,加了inline的函数编译器可以选在在调用的地方不展开,不同编译器处理inline的方法和场景不同,C++并没有对这些进行限制标准。内联函数一般用于代码短小,频繁带哦用的函数。内联函数易错点含有循环语句、含有递归语句、含代码量大,满足这三点其一,都不能称之为内联函数,加上inline后会被编译器忽略编译器的防御手段

若有1w次调用一个100行的内联函数的地方,内联函数展开后,代码量变为 10000 * 100 = 100w行。

函数被编译后是一堆指令需要存储起来执行,内联展开可能会导致可执行程序变大,内存炸开,指令膨胀


C++设计内联函数的设计目的是为了替代C语言的宏函数,C语言里的宏函数容易出错,需要频繁使用括号,不方便调试。inline函数不支持声明和定义分离到两个文件,因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,所以一般都是直接在源文件中定义内联函数的。

//#define ADD(a, b) a + b;

//#define ADD(a, b) (a + b)

//#define ADD(a, b) ((a) + (b))

// 为什么不能加分号? 例如if语句调用加分号的ADD函数

// 为什么要加外⾯的括号,为什么要加⾥⾯的括号?

C语言debug版本默认不展开inline。debug版本想要展开需要设置这两点。

第一步找到vs编译器的 项目,点击点击项目内容的最后一个选项。

找到C/C++选项里的常规,将调试信息格式,设置为 程序数据库(/Zi)

在这里插入图片描述

找到C/C++选项里的优化,将内联函数拓展设置为 只适用 _inline(/Ob1)

在这里插入图片描述

nullptr

NULL是一个宏,在C语言头文件里(stdd.f)可以观察。

<code>#ifndef NULL

#ifdef __cplusplus

#define NULL 0

#else

#define NULL ((void*) 0)

#endif

#endif

C++中的NULL被定义为字面常量0,C语言里被定义为为类型指针的常量。

在这种情况下,传递NULL想调用int*版本的f函数,但调用的结果时int版本的函数,为了弥补这样的不足,C++引用了关键字nullptr

void f(int x)

{

cout << "void f(int x)" << endl;

}

void f(int* x)

{

cout << "void f(int* x)" << endl;

}

int main()

{

f(1);

f(NULL);

return 0;

}

在这里插入图片描述

nullptr在C++11中引用的特殊关键字,nullptr是一种特殊类型的字面常量,它可以转换为其它类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。

使用nullptr就可以避免上述情况出现的问题

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。