C++入门基础

C++基础

  • [1. 命名空间(namespace)](#1. 命名空间(namespace))
    • [1.1 命名空间的价值](#1.1 命名空间的价值)
    • [1.2 namespace的定义](#1.2 namespace的定义)
    • [1.3 命名空间的使用](#1.3 命名空间的使用)
  • [2. C++输入&输出](#2. C++输入&输出)
    • [2.1 附加:提高C++IO效率](#2.1 附加:提高C++IO效率)
  • [3. 缺省参数(默认参数)](#3. 缺省参数(默认参数))
    • [3.1 全缺省参数和半缺省参数](#3.1 全缺省参数和半缺省参数)
    • [3.2 缺省参数的使用](#3.2 缺省参数的使用)
  • [4. 函数重载](#4. 函数重载)
    • [1. 参数类型不同](#1. 参数类型不同)
    • [2. 参数个数不同](#2. 参数个数不同)
    • [3. 参数类型顺序不同](#3. 参数类型顺序不同)
  • [5. 引用](#5. 引用)
    • [5.1 引用的概念和定义](#5.1 引用的概念和定义)
    • [5.2 引用的特性](#5.2 引用的特性)
    • [5.3 引用的使用](#5.3 引用的使用)
    • [5.4 引用做返回值](#5.4 引用做返回值)
    • [5.5 const引用](#5.5 const引用)
      • [5.5.1 const常量 vs const引用](#5.5.1 const常量 vs const引用)
      • [5.5.2 普通变量 vs const引用](#5.5.2 普通变量 vs const引用)
      • [5.5.3 常数 vs const引用](#5.5.3 常数 vs const引用)
      • [5.5.4 表达式 vs const引用](#5.5.4 表达式 vs const引用)
      • [5.5.5 类型转换 vs const引用](#5.5.5 类型转换 vs const引用)
      • [5.5.6 疑问](#5.5.6 疑问)
      • [5.5.7 实践意义](#5.5.7 实践意义)
    • [5.6 指针和引用的关系(面试常考)](#5.6 指针和引用的关系(面试常考))
  • [6. inline](#6. inline)
  • [7. nullptr(C++11)](#7. nullptr(C++11))

1. 命名空间(namespace)

1.1 命名空间的价值

在C/C++中,变量、函数和后面学到的类都是大量存在的,整型变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。

使用命名空间的目的,就是对标识符的名称进行本地化,避免命名冲突。

C语言程序,可能会出现以下问题:

c 复制代码
#include<stdio.h>
#include<stdlib.h>
int rand=10;
int main()
{
	//编译报错:"rand": 重定义;以前的定义是"函数"
	printf("%d\n",rand);
	
	return 0;
}

C++引入namespace,就是来解决上述C语言出现的命名冲突问题的。

1.2 namespace的定义

  • 定义命名空间,需要用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可(注,末尾无需加分号";")

    {}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。

  • namespace本质是定义一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以rand就不再冲突了。

  • C++中域有局部域全局域命名空间域类域

域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所以有了域隔离,名字冲突就解决了。

局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。

  • 编译器默认只在局部和全局中找变量/函数/类型,不会去到命名空间找,因此需要指定。

  • namespace只能定义在全局,当然,它还可以嵌套定义。

  • 项目工程多文件中,定义的同名namespace会认为是一个namespace,不会冲突。

  • C++标准库都放在一个叫std(standard)的命名空间中

1.3 命名空间的使用

  1. 编译默认规则
    编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以直接访问命名空间的成员,程序会编译报错。
  2. ❗️命名空间的使用(重点)
    使用命名空间中定义的变量/函数,有三种方式:
  • 指定命名空间访问。
    ✅️项目最推荐
  • using 将命名空间中某个成员展开。
    ✅️项目常用,成员无冲突时推荐
  • using 展开命名空间中的全部成员
    ❌️项目不推荐(易冲突)
    ✅️日常练习程序
  1. 注意事项
  • 在访问命名空间中的东西时,需要用到 :: (域作用限定符)
  • 使用using后,可以直接访问对应的成员

举个简单的例子演示:

(注:后续将命名空间的名字简称为"名字",如文中的fuyo

cpp 复制代码
//命名空间
namespace fuyo
{
        int a = 10;
        int b = 20;
}

错误案例❌️

cpp 复制代码
//直接编译,编译器无法主动查找命名空间里的内容,程序报错
int main()
{
        printf("%d\n",a);
        return 0;
}

正确使用方法✅️

  1. 指定命名空间访问
    形式 - 名字::成员名
cpp 复制代码
//指定命名空间访问
int main()
{
        printf("%d\n", fuyo::a);
        return 0;
}
  1. using 将命名空间中的某个成员展开
    形式 - using 名字 :: 成员名;
cpp 复制代码
//using 将命名空间中的某个成员展开
using fuyo::b; 
int main()
{
        printf("%d\n", fuyo::a);
        printf("%d\n", b);//可直接访问
        return 0;
}
  1. using 展开命名空间中的全部成员(日常练习时使用)
    形式 - using namespace 名字;
cpp 复制代码
//using 展开命名空间中的全部成员
using namespace fuyo;
int main()
{
        printf("%d\n", a);
        printf("%d\n", b);
        return 0;
}

注:

展开头文件和展开命名空间中的成员中的"展开",并不是一个意思。

展开头文件:在预处理的过程中,将头文件中的内容拷贝到当前文件中,也就是"纯文本替换"。

2. C++输入&输出

  • <iostream> 是Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。可以将其类比为C语言的<stdio.h>

  • std::cinistream 类的对象,它主要面向窄字符(narrow character(of type char))的标准输入流。

  • std::coutostream 类的对象,它主要面向窄字符的标准输出流。

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

  • <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)

  • C++的输入输出可以自动识别变量类型

  • 一般日常练习可以使用using namespace std,实际项目开发中不建议这样做

补充:
<<>>的用法:

cpp 复制代码
cout << x;   // 把 x 插入到输出流 - 输出 x 
cin  >> x;   // 从输入流提取数据 - 数据放入 x

记忆方法:

<< 指向输出流 - 输出 变量数据
>> 指向变量 - 数据放入变量

实践

要么指定命名空间,如下:

cpp 复制代码
#include<iostream>

int main()
{
        int i = 10;
        std::cout << i << std::endl;//指定命名空间
        return 0;
}

要么使用using 展开std,如下:

cpp 复制代码
#include<iostream>
using namespace std;//使用using展开
int main()
{
        int i = 10;
        cout << i << endl;
        return 0;
}

C++自动识别的变量类型的体现:

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int a = 0;
	double b = 0.1;
	char c = 'x';
	//可以自动识别变量类型
	cin >> a >> b >> c;//流提取 - 可以一次性识别一个或多个
	cout << a << endl;//流插入
	cout << b << " " << c << endl;

	return 0;
}

2.1 附加:提高C++IO效率

cin和cout,为了兼容C语言,缓冲区绑定等多种原因,与scanf和printf相比起来,效率较低。

因此 在io需求比较高的地方,如部分大量输入的竞赛题中,加入以下3行代码,可以提高C++IO效率。

cpp 复制代码
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

3. 缺省参数(默认参数)

  • 缺省函数:声明或定义函数时,为函数的参数指定一个缺省值。

  • 在调用该函数时,会出现以下情况:

    • 没有指定实参时,使用函数中指定的缺省值(默认值)
    • 指定实参时,就使用指定的实参。
  • ⭐带缺省函数的函数调用,C++规定必须从左往右依次给实参,不能跳跃给实参。

  • ⭐函数声明和定义分离时,缺省函数不能在函数声明和定义中同时出现,规定是函数声明给缺省值

    • 函数声明(头文件):写缺省值
    • 函数定义(源文件):不写缺省值

代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

void Func(int a = 0)//默认为0
{
	cout << a << endl;
}

int main()
{
	Func();//没有传参时,使用参数的默认值(缺省值) - 输出 0
	Func(10);//传参时,使用指定的实参 - 输出 10

	return 0;
}

3.1 全缺省参数和半缺省参数

  • ⭐缺省参数分为全缺省参数半缺省参数
    • 全缺省:全部形参给缺省值。
    • 半缺省:部分形参给缺省值,C++规定,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。

代码如下:

cpp 复制代码
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

//半缺省
void Func2(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

全缺省的使用示例如下:

cpp 复制代码
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

半缺省的使用如下:

cpp 复制代码
//半缺省
void Func2(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

注:使用半缺省参数时,不可以什么都不传!程序会报错!

3.2 缺省参数的使用

代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

#include"stack.hpp"

int main()
{
	fuyo::ST s1;
	fuyo::STInit(&s1);

	//确定知道要插入1000个数据,初始化时一把开好,避免扩容
	fuyo::ST s2;
	fuyo::STInit(&s2, 1000);

	return 0;
}

4. 函数重载

函数重载:C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,以下三个条件,满足其一即可。

  • 参数类型不同
  • 参数个数不同
  • 参数类型顺序不同

这样C++函数调用就表现出了多态行为,使用更灵活。

注:返回值不同不算重载!

1. 参数类型不同

cpp 复制代码
#include<iostream>
using namespace std;

//1. 参数类型不同
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}

double Add(double left, double right)
{
	cout << "double Add(int left, double right)" << endl;
	return left + right;
}

int main()
{
	Add(1, 2);
	Add(1.1, 2.2);
	return 0;
}

2. 参数个数不同

cpp 复制代码
//2. 参数个数不同
void f()
{
	cout << "f()" << endl;
}

void f(int a)
{
	cout << "f(int a)" << endl;
}

3. 参数类型顺序不同

cpp 复制代码
//3. 参数类型顺序不同
void f(int a, char b)
{
	cout << " f(int a, char b)" << endl;
}

void f(char b, int a)
{
	cout << " f(char b, int a)" << endl;
}

5. 引用

5.1 引用的概念和定义

引用 不是定义一个新变量,而是给已存在变量取了个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

⭐省流:**引用的概念其实就是给变量取别名,不会额外开辟空间 **。

引用的符号:&(虽然和取地址的符号一样,但是&放到类型后则表示引用)

使用方法:
类型& 引用别名 = 引用对象;

代码示例:

cpp 复制代码
int main()
{
	int a = 0;
	//引用:b和c是a的别名
	int& b = a;
	int& c = a;
	//也可以给别名b取别名,d相当于还是a的别名
	int& d = b;

	++d;//a,b,c,d都会增加

	//这里取地址,可以观察到四者的地址一致
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;

	return 0;
}

5.2 引用的特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用(别名)
  • 引用一旦引用一个实体,再不能引用其他实体,也就是说,引用是不可以改变指向的。
c 复制代码
int a = 10;
//编译报错:"ra":必须初始化引用
//int& ra;
int& b = a;
int& c = b;

int d = 20;
b = d;//这里为赋值操作,将d赋值给了b,连同a和c也被赋值了

5.3 引用的使用

  • 引用在实践中的功能:
    • 函数传参和返回值 中减少拷贝效率;
    • 改变引用对象的同时,改变实体

例如,对于两个数据的交换

在C语言中,形参要改变形参,需要对实参进行取地址,代码如下:

c 复制代码
//C语言中需要对实参进行取地址
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int x = 1, y = 0;
	Swap(&x, &y);
	return 0;
}

但在C++中,我们可以使用引用来进行数据的交换。

cpp 复制代码
void Swap(int& rx, int& ry)
{
	//rx是x的别名,ry是y的别名,别名改变会导致实体改变
	int tmp = rx;
	rx = ry;
	ry = tmp;
}
int main()
{
	int x = 1, y = 0;
	Swap(x, y);
	return 0;
}

指针变量也可以取别名,这样就不用传二级指针了,相对而言简化了程序。

示例如下:

cpp 复制代码
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
// LTNode = struct ListNode
// PNode = struct ListNode*

// 这⾥LTNode*& phead就是给指针变量取别名
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
	PNode newnode = (PNode)malloc(sizeof(LTNode));
	newnode->val = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
	phead = newnode;
	}
	else
	{
		//...
	}
}
int main()
{
	PNode* plist = NULL;
	ListPushBack(plist, 1);
	return 0;
}

5.4 引用做返回值

想要弄懂引用做返回值,首先要搞清楚函数返回值的原理:

函数不能直接将内部变量返回给调用者,而是先生成一个临时对象 ,将内部变量的值拷贝给这个临时对象后,再返回。

临时对象 具有常性,因此调用者不可修改。

引用做返回值,则是返回这个内部变量的别名,调用者可以进行修改。

✅️正确使用案例 :直接修改栈顶元素(如图所示)

❗️注意:
引用做返回值不可以取代普通的返回。

引用做返回值,是有条件的。

  • 引用必须绑定在一个,出函数后,不会被销毁的对象。

❌️错误案例:

cpp 复制代码
int& func()
{
	int a = 0;
	return a;//返回局部变量引用,错误!
}

也就是说,出函数后就被销毁的局部变量不可以使用引用返回

⭐不能使用局部变量做引用返回值的原因:

局部变量出函数后就销毁,内存被回收,而它的引用,则指向一块废弃的内存空间 ,此时的引用 类似于野指针,会出现运行问题。

5.5 const引用

⭐引用的核心规则:权限只能缩小,不能放大。

  • 权限缩小:普通变量 → const引用

可读可写 → 只读

  • 权限不可扩大:const常量、常数、表达式、类型转换 → const引用

只读 → 只读

5.5.1 const常量 vs const引用

  • const常量也可以被引用,但是必须使用const引用

❌️错误案例

cpp 复制代码
const int a = 10;
int& ra = a; 

这是因为,引用的时候,权限可以缩小,但不能放大。

被const限制的常量a,权限为只读不可写,若直接引用,相当于扩大它权限,使其变为可读可写,而这绝对是违规的。

因此我们需要使用一个,同样被const限制的引用ra,来给它取别名(如下)

✅️正确示范

cpp 复制代码
const int a = 10;
const int& ra = a;

5.5.2 普通变量 vs const引用

  • 我们可以使用const引用,来给变量取别名,而这就体现了引用权限可以缩小的特点。
    示例如下:
cpp 复制代码
//引用权限的缩小
int b = 10;//b是变量可以被修改,b的变化也会造成rb的变化
const int& rb = b;//rb被const限制,不可以被修改

5.5.3 常数 vs const引用

  • const引用的对象可以是常数(注,普通引用不可以引用常数)
cpp 复制代码
int& rc =30;//❌️错误案例
const int& rd = 30;//✅️正确示范

5.5.4 表达式 vs const引用

  • 注意,表达式的返回值同样存储在临时对象中,临时对象具有常性。

临时对象:

编译器需要一个空间暂存表达式的求值结果 时,临时创建的一个未命名的对象。

C++中,把这个未命名对象叫做临时对象。

  • 因此,若想给表达式的返回值取别名,也需要使用const引用。
cpp 复制代码
int a = 5;
int b = 10;

int& rc = a + b;//❌️错误案例
const int& rd = a + b;//✅️正确示范
//a+b的是一个表达式,其求值结果具有常性,需要const引用!

5.5.5 类型转换 vs const引用

  • 使用不同于本体的数据类型,进行赋值/引用 时,会发生类型转换,也会产生一个具有常性的临时对象。
  • 所以,需要使用 const引用 给其取别名。
cpp 复制代码
double a = 3.14;
int b = a;

int& rc = a;//❌️错误案例
const int& rd = a;//✅️正确示范
//类型转换会产生临时对象,需要使用const引用

5.5.6 疑问

❓ask:用const引用,给临时对象取别名,临时对象一销毁,const引用不就出问题了吗?

⭕答:

不会。当const引用临时对象时,临时对象的生命周期就会和const引用绑定,const引用销毁,临时对象才会被销毁。

5.5.7 实践意义

const引用常被用于函数传参中(不需要利用形参改变实参时)。

原因如下:

  • const引用可以扩大传参范围,它可以接收普通变量、const常量、表达式、类型转换。
  • 引用可以减少内存拷贝。
  • 保证数据安全,函数内部不会改变对象。

5.6 指针和引用的关系(面试常考)

C++中指针和引用像两个性格迥异的亲兄弟,在实践运用中,二者相辅相成,功能有重叠性,但是各有自己的特点,互相不可取代。

  • 语法概念上,引用是一个变量的取别名,不开空间;指针是存储一个变量的地址,要开空间。
  • 引用在定义时,必须初始化;指针建议初始化。
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象。
  • sizeof中含义不同。引用结果为引用类型的大小;而指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
  • 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对安全一点(ps:不是绝对安全)

6. inline

  • 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。

  • inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。

  • inline适用于被频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。

  • C语言实现宏定义函数时,也会在预处理阶段替换展开,但是,宏函数实现复杂又容易出错,且不方便调试,C++设计inline目的就是替代C的宏函数。

  • vs编译器debug版本下,默认不展开inline,这样方便调试。debug版本想展开需要设置以下两个地方:

    • 打开"属性",找到C/C++ 底下的"常规",将"调试信息格式"改为"程序数据库"
    • 在C/C++ 底下的"优化"中,将内联函数扩展,改为"inline"

inline不展开 = 虽然写了inline,但编译器依然把它当普通函数调用。

inline展开 = 将函数体"复制粘贴"到调用处,不执行函数调用的过程。

展开的优缺点:

  • 优点:

    • 消除函数调用开销,提升程序运行速度。适用于简短函数。
  • 缺点:

    • 不可用于递归和复杂的大函数,内联的大函数展开,会导致可执行程序体积暴增。

例如:100条指令,有10000个位置调用

不展开的指令数:10000+100条

展开的指令数:10000*100条(可执行程序体积暴增!)

  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开时,没有函数地址,链接时会出现报错。

❌️错误案例:

cpp 复制代码
//F.h
#include<iostream>
using namespace std;

inline void f(int i);

#include"F.h"
void f(int i)
{
	cout << i << endl;
}

//main.cpp
#include"F.h"
int main()
{
	f(10);//链接错误:无法解析的外部符号
	return 0;
}

原因:

声明和定义分离到两个文件时,main.cpp只能找到内联函数的声明,函数体在F.cpp中,不仅无法链接,也会丧失内联性质

✅️正确示范:内联直接放到.h文件里

cpp 复制代码
//F.h
#include<iostream>
using namespace std;

inline void f(int i)
{
	cout << i << endl;
}

//main.cpp
int main()
{
	f(10);
	return 0;
}

7. nullptr(C++11)

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到以下代码:

cpp 复制代码
#ifndef NULL
	#ifdef __cplusplus
		#define NULL 0
	#else
		#define NULL ((void *)0)
	#endif
#endif

在C语言中,允许void*的指针隐式类型转换成其他类型的指针。

但C++十分严格,void*的指针必须进行强制类型转换后才能转换。

✅️C语言:允许void*指针隐式类型转换,不会报错

c 复制代码
//C语言
void* pa = NULL;
int* pb = pa;//不会报错

❌️C++:禁止隐式类型转换(必须使用强制类型转换)

cpp 复制代码
//C++
void* pa = NULL;
int* pb = pa;
//无法将void* 直接转换成int*

✅️C++:允许强制类型转换

cpp 复制代码
//C++
void* pa = NULL;
int* pb = (int*)pa;
  • C++11中引入nullptrnullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型
  • 使用nullptr定义空指针可以避免类型转换的问题,因为nullptr能被隐式类型转换 为指针类型。(注:nullptr不能被转换为整型)
相关推荐
t***5446 小时前
如何配置Orwell Dev-C++使用Clang
开发语言·c++
CoderCodingNo6 小时前
【信奥业余科普】C++ 的奇妙之旅 | 13:为什么 0.1+0.2≠0.3?——解密“爆int”溢出与浮点数精度的底层原理
开发语言·c++
极客智造8 小时前
深入详解 C++ 智能指针:RAII 原理、分类特性、底层机制与工程实战
c++·智能指针
王璐WL9 小时前
【C++】类的默认成员函数(上)
c++
王老师青少年编程9 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:区间覆盖(加强版)
c++·算法·贪心·csp·信奥赛·区间贪心·区间覆盖(加强版)
宏笋9 小时前
C++11完美转发的作用和用法
c++
格发许可优化管理系统9 小时前
MathCAD许可类型全面解析:选择最适合您的许可证
c++
旖-旎10 小时前
深搜(二叉树的所有路径)(6)
c++·算法·leetcode·深度优先·递归
GIS阵地10 小时前
QGIS的分类渲染核心类解析
c++·qgis·开源gis
凯瑟琳.奥古斯特11 小时前
C++变量与基本类型精解
开发语言·c++