01_C++ 入门基础

!\[Pasted image 20260808210409.png]

1. 命名空间

C++ 复制代码
#include<stdio.h>
#include<stdlib.h>

//  定义在全局域
int rand = 10;

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

int main()
{
	//  定义在函数局部域
	int rand = 1;
	printf("%d", rand);
	
	//  访问全局变量
	printf("%d", ::rand);
	return 0;
}

编译器查找一个变量/函数/类型的声明或定义时,默认逻辑是:先查找局部域,再查找全局域

在 C++ 中,变量、函数以及类都是大量存在的,默认情况下它们的名称都存在于全局作用域中,这极易导致命名冲突。

因此,使用 namespace 的目的是对这些标识符的名称进行本地化,以避免命名冲突和名字污染

1.1. C++ 中的域

C++ 中常见的域包括:

  1. 局部域 (Local Scope)
  2. 全局域 (Global Scope)
  3. 命名空间域 (Namespace Scope)
  4. 类域 (Class Scope)

变量的生命周期:

• 局部域 & 全局域:不仅影响查找,还会决定变量的生命周期(如局部自动变量,随栈帧创建和销毁)。

• 命名空间域 & 类域:只影响编译查找,不影响变量的生命周期。定义在 namespace 中的变量生命周期与全局变量一致(位于静态存储区,贯穿整个程序运行期)。

1.2. namespace

定义命名空间,需要使用 namespace 关键字。命名空间中可以定义变量、函数、类型等。

C++ 复制代码
namespace namespace_name
{
    //成员
}

namespace 的本质是定义一个命名空间域,这个域跟全局域各自独立,不同的域可以定义同名变量

1.3. 命名空间的使用

编译时查找一个变量的定义/声明时,默认只会在局部域或全局域中寻找。我们要使用命名空间中定义的变量/函数,有以下三种方式:

  • 指定命名空间访问 (项目中推荐的方式)

    使用域作用限定符::

  • 展开命名空间中的全部成员 (项目不推荐,冲突风险较大)

    使用 using namespace 指令展开命名空间,这样在使用命名空间时就可以不用在前面加上命名空间的名称。

C++ 复制代码
using namespace namespace_name;
C++ 复制代码
#include<iostream>
using namespace std;

namespace space
{
    int a = 0;
    int b = 1;
}

using namespace space;

int main()
{
    cout << a << endl;
    return 0;
}
  • 展开命名空间中的某个成员 (项目中经常访问的不存在冲突的成员使用)
    使用 using 指令展开命名空间中的某个成员。
C++ 复制代码
using namespace_name::namespace_member_name;
C++ 复制代码
#include<iostream>
using namespace std;
namespace space
{
    int a = 0;
    int b = 1;
}
using space::a;
int main()
{
    cout << a << endl;
    cout << space::b << endl;
    return 0;
}

注意:

使用using展开时,在存在同名变量的情况下:

  1. 若存在局部域变量,则局部域变量的优先级最高;
  2. 若仅存在全局域变量与命名空间变量,会报错。
C++ 复制代码
#include<iostream>
using namespace std;

int a = 10;

namespace space
{
    int a = 0;
    int b = 1;
}

using namespace space;

int main()
{
	//  存在局部变量
    int a = 20;
    cout << a << endl;//输出 20
    return 0;
}
C++ 复制代码
#include<iostream>
using namespace std;

int a = 10;

namespace space
{
    int a = 0;
    int b = 1;
}

using namespace space;

int main()
{
    cout << a << endl;
    //编译报错: "a": 不明确的符号
    return 0;
}

2. 输入 & 输出

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

注意:

C++ 中,没有包含 <stdio.h> ,也可以使⽤ printfscanf ,在 <iostream> 中间接包含了。vs系列编译器是这样的,其他编译器可能会报错。

在 C++ 中,标准库都放在名为 std 的命名空间中。

  • std::cinistream 类的全局对象 ,它主要面向窄字符(char类型的变量)的标准输入流
  • std::coutostream 类的全局对象 ,它主要面向窄字符的标准输出流
  • std::endl 是一个函数,在流插入输出时,相当于插入一个换行符并刷新缓冲区
  • <<是流插入运算符,>>是流提取运算符号。

C++ 的输入输出方式相较于 C 语言的 scanfprintf ,C++ 的输入输出可以自动识别变量类型 (本质通过函数重载实现),并且能够更好的支持自定义类型对象的输入和输出

这是因为在 C++ 中,已经完成了内置类型输入输出 的实现,并且支持对自定义类型的运算符重载

C++ 复制代码
#include<iostream>
using namespace std;
int main()
{
    int a = 0;
    double b = 0.1;
    char c = 'x';
    cin >> a >> b >> c;
    cout << a << " " << b << " " << c << endl;
    return 0;
}
C++ 复制代码
#include<iostream>
using namespace std;
int main()
{
    // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
    // 可以提⾼C++IO效率
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
}

3. 缺省参数

缺省参数是声明或定义函数时为函数的参数指定一个缺省值

在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参。

缺省参数分为全缺省半缺省。(有些地方把缺省参数也叫默认参数)

  • 全缺省就是全部形参 给缺省值,半缺省就是部分形参 给缺省值。C++ 规定,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
  • 带缺省参数的函数调用,C++ 规定必须从左到右依次给实参,不能跳跃给实参。
  • 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现 ,规定必须在函数声明 中给缺省值。
    编译器在看到函数调用的时候,只会查看函数声明,不会去找函数定义。默认参数实际上不是函数的一部分,而是编译器在调用时帮你补上。
C++ 复制代码
#include<iostream>
using namespace std;
void Func1(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}
void Func2(int a, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}
int main()
{
    Func1();
    Func1(1);
    Func1(1, 2);
    Func1(1, 2, 3);
    //不支持Func1(,,3)
    Func2(1);
    Func2(1, 2);
    Func2(1, 2, 3);
}

例如在栈的实现中,我们便可以通过使用缺省参数来简化栈的初始化。

C++ 复制代码
Stack.h:

#include<iostream>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
    STDataType* arr;
    int top;
    int capacity;
}ST;
void STInit(ST* ps, int n = 4);

////////////////////////////////////////////////////////////////////////

Stack.cpp:

#include<Stack.h>

//  缺省参数不能声明和定义同时给
void STInit(ST* ps, int n)
{
    assert(ps);
    STDataType* tmp = (STDataType*)
    malloc(n * sizeof(STDataType));
    if (tmp == NULL)
    {
        perror("malloc");
        exit(1);
    }
    ps->arr = tmp;
    ps->top = 0;
    ps->capacity = n;
}

////////////////////////////////////////////////////////////////////////

test.cpp

#include"Stack.h"

int main()
{
	//  不知道要开多少空间
	ST S1;
	STInit(&s1);
	//  确定要插入1000个数据,初始化时直接开好,避免后续扩容,提升代码运行效率
	ST S2;
	STInit(&s2, 1000);
}

4. 函数重载

C 语言 是不支持同一作用域中出现同名函数的。

C++ 支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同 ,可以是参数个数不同类型不同 或者参数类型顺序不同(本质还是类型不同)。

C++ 复制代码
// 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(double left, double right)" << endl;
    return left + right;
}

// 2、参数个数不同
void f()
{
    cout << "f()" << endl;
}
void f(int a)
{
    cout << "f(int a)" << endl;
}

// 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;
}

易错点:

  1. 返回值不同不能作为重载条件,因为调用时没法区分。
C++ 复制代码
//  不够成重载
void fxx()
{}

int fxx()
{
        return 0;
}
  1. 两个重载函数的调用入口不能重叠
C++ 复制代码
//  下面两个函数构成重载,但是调用时,会报错
//  在不输入参数的情况下,存在歧义,编译器不知道调用谁
void f1()
{
    cout << "f()" << endl;
}
void f1(int a = 10)
{
    cout << "f(int a = 10)" << endl;
}
C++ 复制代码
//  在输入参数的情况下,存在歧义,编译器不知道调用谁
void f1(int a = 10)
{
    cout << "f(int a = 10)" << endl;
}
void f1(int a)
{
    cout << "f(int a)" << endl;
}

5. 引用

5.1. 概念与定义

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

C++ 复制代码
类型 & 引用别名=引用对象;
C++ 复制代码
#include<iostream>
using namespace std;
int main()
{
    int a = 0;
    int& b = a;
    int& c = b;//c也相当于是a的引用
    c++;
    
    //a,b,c的值相同
    cout << a << " " << b << " " << c << endl;
    
    //a,b,c的地址相同
    cout << &a << endl;
    cout << &b << endl;
    cout << &c << endl;
    return 0;
}

5.2. 引用的特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用不能改变指向,一旦引用一个实体,就不能再引用其他实体
C++ 复制代码
#include<iostream>

using namespace std;

int main()
{
	int a = 10;

	//  编译报错:引用 变量 "ra" 需要初始值设定项
	//  int& ra;

	int& b = a;

	int c = 20;

	//  并非让b引用c,引用不能改变指向
	//  是一个赋值操作
	b = c;

	cout << a << " " << b << " " << c << endl;

	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
}

5.3. 引用的使用

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

  • 引用传参和指针传参的功能是类似的,且引用传参相对更加方便。
C++ 复制代码
//  函数重载 + 引用,简化程序,使用更加便捷
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

void Swap(double& x, double& y)
{
	double tmp = x;
	x = y;
	y = tmp;
}
  • 在部分 C 语言实现的数据结构中,可以使用 C++ 引用代替指针传参,在需要使用二级指针等复杂的指针时,可以简化程序。
C++ 复制代码
#include<iostream>

using namespace std;

typedef int SLTDataType;

typedef struct ListNode
{
	SLTDataType val;
	ListNode* next;
}LTNode, *PNode;


//  不用二级指针,可以简化程序
//  void LTPushBack(LTNode** phead, SLTDataType x)
//  void LTPushBack(LTNode*& phead, SLTDataType x)
void LTPushBack(PNode& phead, SLTDataType x)
{
	PNode newnode = (PNode)malloc(sizeof(LTNode));
	newnode->val = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//...
	}
}

int main()
{
	PNode plist = NULL;
	LTPushBack(plist, 1);
	return 0;
}
  • 引用作返回值,返回的是返回对象的别名(引用),能够对返回对象进行修改,也可以减少拷贝,提高效率。
    传值返回会调用拷贝构造产生一个临时对象(右值)作为返回值,具有常性。因为大部分情况下,函数返回的是一个局部变量。
C++ 复制代码
#include<iostream>
#include<assert.h>

using namespace std;

typedef int STDataType;

typedef struct Stack
{
	STDataType* arr;
	int top;
	int capacity;
}ST;

void STInit(ST& rs, int n = 4)
{
	rs.arr = (STDataType*)malloc(sizeof(STDataType) * n);
	rs.top = 0;
	rs.capacity = n;
}

void STPush(ST& rs, STDataType x)
{
	if (rs.top == rs.capacity)
	{
		int newcapacity = 2 * rs.capacity;
		STDataType* tmp = (STDataType*)realloc(rs.arr, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);
		}
		rs.arr = tmp;
		rs.capacity = newcapacity;
	}
	rs.arr[rs.top++] = x;
}

//  int STTop(ST& rs)
//  编译报错:"+=" :左操作数必须为左值
//  返回值为拷贝的临时变量,是一个右值,具有常性

//  返回的是引用,是一个左值,能够修改返回对象
int& STTop(ST& rs)
{
	assert(rs.top > 0);

	return rs.arr[rs.top - 1];
}

int main()
{
	ST st;
	STInit(st);
	STPush(st, 1);
	STPush(st, 2);

	STTop(st) += 10;

	cout << STTop(st) << endl;
	
	return 0;
}

注意:不是所有的函数都可以将引用作为返回值。

传引用返回是有风险的。

如果返回对象是一个当前函数局部域的局部对象 ,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用。野引用会导致很多奇怪的报错,且不同的编译器的报错可能不同。

传引用返回一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

思考:

缺省参数,函数重载,引用,以及后面的运算符重载的意义是什么?

C++ 复制代码
#include<iostream>
#include<assert.h>

using namespace std;

#define N 100
typedef int SLTDataType;
struct SeqList
{
	SLTDataType arr[N];
	int size;

	void Init()
	{
		size = 0;
	}

	void PushBack(int x)
	{
		assert(size < N);
		arr[size++] = x;
	}

	//  返回指定位置的值
	int& AT(int i)
	{
		assert(i < size);
		return arr[i];
	}

	int& operator[](int i)
	{
		assert(i < size);
		return arr[i];
	}
};

int main()
{
	SeqList s;
	s.Init();
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	//  想让偶数位置的值 * 2
	//  写法1
	for (int i = 0; i < s.size; i++)
	{
		if (i % 2 == 0)
		{
			s.AT(i) *= 2;
		}
	}
	for (int i = 0; i < s.size; i++)
	{
		cout << s.AT(i) << endl;
	}

	//  写法2
	for (int i = 0; i < s.size; i++)
	{
		if (i % 2 == 0)
		{
			s[i] *= 2;
		}
	}
	for (int i = 0; i < s.size; i++)
	{
		//  cout << s.operator[](i) << endl;
		//  编译器转化为下面的函数调用
		cout << s[i] << endl;
	}

	return 0;
}

显然,写法2 的可读性更强。

通过引用和运算符重载,实现了让自定义的数据结构支持类似数组的访问方式

使代码更加直观,提高了代码的可读性和易用性,让自定义类型的使用体验更加接近内置类型

5.4. const 引用

  • 引用一个 const 对象必须用 const 引用,但是 const 引用可以引用普通对象。这是因为对象的访问权限可以缩小,但不能变大
C++ 复制代码
#include<iostream>

using namespace std;

int main()
{
    const int a = 10;
    const int& ra = a;
    //  权限不能放大
    //  编译报错:"初始化" :无法从"const int"转换为"int& "
    //  int& ra = a;

    int b = 20;
    //  权限可以缩小
    const int& rb = b;

    int c = a;
    //  这里是赋值操作
    //  不涉及权限的放大和缩小

    return 0;
}
  • 临时对象就是编译器需要一个空间暂存值时临时创建的一个未命名的对象, C++ 中把这个未命名对象叫做临时对象。
    • 函数传值返回表达式运算类型转换会产生临时对象。
    • C++ 中规定,临时对象具有常性 ,因此使用普通引用将会触发权限放大,必须要使用常引用
C++ 复制代码
#include<iostream>

using namespace std;

int main()
{
    int a = 10;
    const int& ra = 3 * a;
    //编译报错: "初始化" 无法从"int"转换为"int& "
    //int& ra = 3 * a;

    double b = 12.34;
    const int& rb = b;
    //编译报错: "初始化" :无法从"double"转换为"int& "
    //int& rb = 3 * b;

    return 0;
}
C++ 复制代码
#include<iostream>

using namespace std;

typedef int type;

void Test(const int& x)
{
    //  void Test(int& x)
    //  对不改变参数的引用,一般会加上 const
    //  如果不加上const,Test(a)会报错
    //  编译报错:无法从"const int"转换为"int &"

}

int main()
{
    const int a = 10;
    int b = 20;
    
    Test(a);
    Test(b);
    
    return 0;
}

5.5. 指针和引用

C++ 中指针和引用的功能有重叠性,但是各有自己的特点,互相不可替代。

  • 语法概念上,引用是一个变量的取别名,不开辟新的空间,而指针是存储一个变量地址,需要开辟新的空间。
  • 汇编层面,引用是用指针实现的,没有引用,只有指针。引用和指针都需要开辟新的空间。
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
  • sizeof中,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。

6. inline

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

C语言 中,实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试。

C++ 设计了 inline 目的就是替代 C 语言 的宏函数。 在 C++ 中,通常会使用 inline 代替 C语言 中的宏函数,使用 constenum 代替 C语言 中的宏常量。

C++ 复制代码
实现一个宏函数的常见错误:
#define ADD(int a, int b) return a + b;
#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)

正确写法:
#define ADD(a, b) ((a) + (b))
  
为什么不能加分号?
	宏本质是文本替换。
	避免宏展开后产生额外语句,影响语法结构。
	#define ADD(a, b) ((a) + (b));
	if (1)
	  ADD(1, 2);
	else
	  cout << "error";
	替换后:
	if (1)
	  ADD(1, 2);
	;
	//  error: else without a previous if
	else
	  cout << "error";
 
为什么要加外面的括号?
	保证宏整体作为一个表达式,避免运算符优先级问题。
	#define ADD(a,b) (a) + (b)    
	int x = ADD(1,2) * 3;
	替换后:
	int x = (1) + (2) * 3;

为什么要加里面的括号?
	保护传入的参数,避免运算符优先级问题。
	#define ADD(a,b) (a + b)    
	ADD(1 << 2, 3)
	替换后:
	(1 << 2 + 3) -> 1 << (2 + 3)
  • inline对于编译器而言只是一个建议 ,也就是说,加了inline,编译器也可以选择在调用的地方不展开 。不同编译器关于inline什么情况展开的情况各不相同,因为 C++ 标准没有规定。一般而言,inline适用于频繁调用的短小函数 。对于递归函数或代码多些的函数,加上inline,也会被编译器忽略。
  • inline 不建议声明和定义分离到两个文件 ,分离会导致链接错误。因为 inline 被展开而不是调用,因此编译时内联函数不会进入符号表,就找不到内联函数的函数地址,链接时会出现报错 。因次,内联函数一般直接定义在 .h 文件中。

  • vs2022debug 版本下默认是不展开 inline 的,这样方便调试,debug 版本想展开,需要设置一下 以下两个地方。

为什么 inline 只是一个建议?

因此,为了避免程序员滥用内联,内联只是给编译器的一个建议。

7. nullptr

NULL 实际是一个宏,在 stddef.h 中,可以看到以下代码:

C++ 复制代码
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0

    #else
        #define NULL ((void*)0)

    #endif
#endif 
  • C++ 中 NULL 可能被定义为字面常量 0,或者在 C 中被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,不可避免的会遇到一些麻烦。
  • C++11 中引入 nullptrnullptr 是⼀个特殊的关键字,nullptr 是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被隐式地转换为指针类型,而不能被转换为整数类型。
C++ 复制代码
#include<iostream>
using namespace std;
void f(int x)
{
    cout << "f(int x)" << endl;
}
void f(int* ptr)
{
    cout << "f(int* ptr)" << endl;
}
int main()
{
    f(0);

    f(NULL);
    //  本想通过f(NULL)调⽤指针版本的f(int*)函数
    //  但是由于NULL被定义成0,调⽤了f(intx),因此与程序的初衷相悖。

    f((int*)NULL);
    //  需要强制类型转换才能调用函数
    //  f((void*)NULL);
    //  编译报错: "f": 2个重载中没有⼀个可以转换所有参数类型

    f(nullptr);
    
    return 0;
}
相关推荐
郝学胜-神的一滴3 小时前
中级OpenGL教程 030:gl_FragCoord解锁区域渲染与深度可视化新姿势
c++·unity·游戏引擎·图形渲染·unreal engine·opengl
不会代码的小猴3 小时前
11. 类和动态内存分配
开发语言·c++
鱼子星_4 小时前
【C++】stack和queue的应用及其模拟实现:适配器与容器适配器
数据结构·c++·笔记·stl
jufeng13074 小时前
【系列:MiniKV 原理剖析 · 第 2 篇】
linux·c++·软件工程
charlie1145141915 小时前
IMX6ULL WM8960 的移植——前置介绍
开发语言·c++·开源项目·嵌入式linux
charlie1145141915 小时前
Cinux是如何管理进程的 —— 上下文与调度
开发语言·c++·操作系统·开源项目
hold?fish:palm6 小时前
redis中AOF 重写机制解析
数据库·c++·redis
阿米亚波6 小时前
【C++ 异常处理】try-catch
开发语言·c++·笔记·try-catch
旖旎夜光6 小时前
C++(内存管理)
开发语言·c++·学习