个人专栏:《数据结构-初阶》《经典OJ题目》《C语言》《小白算法成长录》

欢迎大佬交流

本文代码已同步GitHub:GitHub - Stellen-z/DailyCode: pracetice · GitHub

一、从C语言到C++

1、为什么学习C++

C++是在C语言基础上发展出来的语言,它不仅保留了C的:

  • 高性能
  • 直接操作内存的能力
  • 适合底层开发的特性

还增加了:

  • 面向对象编程(OOP)
  • 泛型编程(模板)
  • 标准模板库(STL)

简单理解:

C是“工具”,C++是“工具箱 + 设计系统”

学习C++的意义是:

  • 从“过程式思维” → “结构化 + 对象化思维”
  • 从“写单个函数” → “设计完整程序结构”
  • 为后续高阶数据结构与算法打基础

2、C和C++的区别

C语言:面向过程编程

C语言以“函数”为核心,把问题拆解为一个个步骤:

  • 程序 = 函数 + 数据
  • 强调“怎么做”(过程)

C++:多范式语言(面向对象为主)

C++支持:

  • 面向过程(兼容C)
  • 面向对象(OOP)
  • 泛型编程(模板)

更强调:

“谁来做这件事”(对象)

 C语言不支持:

  • 没有类(class)
  • 没有对象
  • 没有封装 / 继承 / 多态

 C++支持:

  • class / struct(增强版)
  • 封装(封装数据和方法)
  • 继承(代码复用)
  • 多态(接口统一)

3、第一个C++程序

首先明确C++兼容C绝大多数的语法,因此我们先在C++环境中编译C代码:

#include <stdio.h>

int main()
{
	printf("Hello World!\n");
	return 0;
}

程序正常运行!

当然,C++有自己的输入输出,肯定不会像C语言这样写

#include <iostream>

using namespace std;

int main()
{
	cout << "Hello World!" << endl;
	return 0;
}

不知道各位小伙伴用C++/Java第一次写出Hello World时,和第一次用C语言写出来的Hello World有什么不同的感觉呢?

下面我们就从C++的第一个程序开始学习基础语法!

二、C++基础语法

1、namespace

我们先来看一段C语言程序:

#include <stdio.h>
#include <stdlib.h>

int rand = 10;

int main()
{
	//error C2365: “rand”: 重定义;以前的定义是“函数”
	printf("%d\n", rand);
	return 0;
}

直接编译错误!

报错原因是重定义,当我们想打印 rand 时,在C语言的编译环境下、

rand是一个函数,而我们在全局变量中又定义了rand,导致rand的重定义!

那么在C++中,我们该怎么避免这种情况呢?
答案就是使用 namespace--命名空间

namespace 的定义

● 定义命名空间,需要使用namespace 关键字,后面跟命名空间的名字,然后加上一对{ },{ }中即为命名空间的成员;可以定义变量、函数、类等;

● namespace 本质是定义一个域,这个域跟全局域各自独立,而不同的域可以定义同名变量,因此基本解决了命名冲突

● C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑;

因此,有了域隔离,名字冲突就解决了;

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

● namespace 只能定义在全局,当然也可以嵌套定义

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

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

下面我们来定义上述的例子

//1.命名空间定义
#include <stdio.h>
#include <stdlib.h>


namespace stn
{
	//定义变量
	int rand = 20;
	//定义函数
	int Add(int x, int y)
	{
		return x + y;
	}
}

int main()
{
	printf("%p\n", rand);//默认访问rand函数指针
	printf("%d\n", stn::rand);//访问stn命名空间中的rand

	return 0;
}

//2.命名空间的嵌套
#include <stdio.h>
#include <stdlib.h>

namespace mn
{
	//pq命名空间
	namespace pq
	{
		int rand = 1;
		int Add(int x, int y)
		{
			return x + y;
		}
	}
	//xy命名空间
	namespace xy
	{
		int rand = 1;
		int Add(int x, int y)
		{
			return x + y;
		}
	}
}

int main()
{
	printf("%d\n", mn::pq::rand);
	printf("%d\n", mn::xy::rand);

	printf("%d\n", mn::pq::Add(1, 2));
	printf("%d\n", mn::xy::Add(1, 2));


	return 0;
}

//多文件可以重复定义同名的namespace,他们会默认合并到一起
//Stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

namespace stn
{
	// 支持动态增长的栈
	typedef int STDataType;
	typedef struct Stack
	{
		STDataType* _a;
		int _top;		// 栈顶
		int _capacity;  // 容量 
	}Stack;

	// 初始化栈 
	void StackInit(Stack* ps);

	// 入栈 
	void StackPush(Stack* ps, STDataType data);

	// 出栈 
	void StackPop(Stack* ps);

	// 获取栈顶元素 
	STDataType StackTop(Stack* ps);

	// 获取栈中有效元素个数 
	int StackSize(Stack* ps);

	// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
	bool StackEmpty(Stack* ps);

	// 销毁栈 
	void StackDestroy(Stack* ps);
}

//Stack.cpp
#include "Stack.h"

namespace stn
{
	// 初始化栈 
	void StackInit(Stack* ps)
	{
		assert(ps);
		ps->_a = NULL;
		ps->_capacity = ps->_top = 0;
	}

	// 入栈 
	void StackPush(Stack* ps, STDataType data)
	{
		assert(ps);
		if (ps->_capacity == ps->_top)
		{
			int newcapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
			STDataType* tmp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * newcapacity);
			if (tmp == NULL)
			{
				perror("realloc failed!\n");
				exit(1);
			}

			ps->_a = tmp;
			ps->_capacity = newcapacity;
		}

		ps->_a[ps->_top++] = data;
	}

	// 出栈 
	void StackPop(Stack* ps)
	{
		assert(ps);
		assert(ps->_top > 0);

		ps->_top--;
	}

	// 获取栈顶元素 
	STDataType StackTop(Stack* ps)
	{
		assert(ps);
		assert(ps->_top > 0);

		return ps->_a[ps->_top - 1];
	}

	// 获取栈中有效元素个数 
	int StackSize(Stack* ps)
	{
		assert(ps);

		return ps->_top;
	}

	// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
	bool StackEmpty(Stack* ps)
	{
		assert(ps);
		return ps->_top == 0;
	}

	// 销毁栈 
	void StackDestroy(Stack* ps)
	{
		free(ps->_a);
		ps->_a = NULL;
		ps->_capacity = ps->_top = 0;

}

}
//Test.cpp
#include "Stack.h"

//定义全局的Stack
typedef struct Stack
{
    int a[10];
    int top;
}ST;

void STInit(ST* ps) {};
void STPush(ST* ps, int x) {};


int main()
{
    //调用全局的栈
    ST st1;
    STInit(&st1);
    STPush(&st1, 1);
    STPush(&st1, 2);
    STPush(&st1, 3);
    printf("%d\n", sizeof(st1));//固定10个int类型,共44字节

    //调用stn的栈
    stn::Stack st2;
    printf("%d\n", sizeof(st2));//一个x64环境下指针,两个int类型,共16字节
    stn::StackInit(&st2);
    stn::StackPush(&st2, 1);
    stn::StackPush(&st2, 2);
    stn::StackPush(&st2, 3);

}

namespace 的使用

当编译器在查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间中查找;

因此下面的程序会报错

#include <stdio.h>

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

int main()
{
	//error C2065: “a”: 未声明的标识符
	printf("%d\n", a);

	return 0;
}

要使用命名空间中定义和变量、函数,有三种方式:

a、指定命名空间,项目中推荐这种方式

int main()
{
	printf("%d\n", stn::a);

	return 0;
}

b、using将命名空间中某个成员展开,项目中经常访问且不存在冲突的成员推荐这种方式

using stn::a;

int main()
{
	printf("%d\n", a);
	printf("%d\n", stn::b);

	return 0;
}

c、展开命名空间全部成员,项目不推荐,冲突风险很大,日常练习可以使用

using namespace stn;

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

	return 0;
}

2、输入输出

在C语言中,包含输入输出的头文件是<stdio.h>

而在C++中,包含输入输出的头文件是<iostream>

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

而在前面我们说过,std这个命名空间里面包含cin 和 cout

std::cin 是 istream类的对象,主要面向的是窄字符(narrow characters)的标准输入流,那么宽字符对应的就是wcin;

std::cout 是 ostream 类的对象,主要面向窄字符的标准输出,那么宽字符对应的就是wcout;

<<是流插入运算符,>>是流提取运算符

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

使用C++输入输出更方便,不需要printf/scanf那样需要手动指定格式,C++的输入输出可以自动识别变量类型

cout/cin/endl等都属于C++标准库,C++标准库都放在std的命名空间中,因此要通过命名空间来使用

因此,在日常练习中我们可以 using namespace std,实际项目开发中不能直接展开

由于C++兼容C大部分语法,因此在编译时,会一并编译C语言的代码

这就导致有时C++的效率会降低

比如当需要处理大量输入输出时,为了提高效率,我们通常会加上下面这三行代码

#include <iostream>

int main()
{
	std::ios_base::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);

	return 0;
}

三行代码的作用:

  1. std::ios_base::sync_with_stdio(false);
    取消 C++ 标准流(cin/cout)与 C 标准流(stdio)之间的同步;默认情况下两者同步,以保证混用时的顺序正确,但会带来额外开销。关闭同步后,cin/cout 效率大幅提升,但不能再混用 printf/scanf,否则会导致未定义行为。

  2. std::cin.tie(nullptr);
    解除 cin 与 cout 的绑定。默认情况下,每次执行 cin 前会自动刷新 cout,解除绑定可减少不必要的刷新开销。

  3. std::cout.tie(nullptr);
    解除 cout 与其他流的绑定(通常无额外绑定,但保持写法对称或防止未来修改)。

三、函数增强

1、缺省参数

缺省参数的定义:允许在函数声明时为参数指定默认值;调用时若省略该实参,则自动使用默认值。

缺省分为全缺省、半缺省;

全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值;

注:C++规定半缺省参数必须从右向左依次连续缺省,不能间隔跳跃给缺省值

函数定义和声明分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值

我们来看下面的例子就能明白

a、缺省参数的使用

#include <iostream>

using namespace std;

void fun(int a = 1)
{
	cout << a << endl;
}

int main()
{
	fun();   //没有传参时,使用参数的默认值
	fun(10); //传参时,使用指定的实参
}

b、缺省参数的分类

#include <iostream>

using namespace std;

//全缺省
void Func1(int a = 1,int b = 2,int c = 3)
{
	cout << a << " " << b << " " << c << endl;
}

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

int main()
{
	Func1();            //1 2 3
	Func1(10);          //10 2 3
	Func1(10,20);       //10 20 3
	Func1(10,20,30);    //10 20 30

	Func2(100);         //100 2 3
	Func2(100,200);     //100 200 3
	Func2(100,200,300); //100 200 300

}

2、函数重载

C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同:可以是参数个数不同或者是类型不同!

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

我们通过代码来观察

#include <iostream>

using namespace std;

namespace stn
{
	//1.参数类型不同
	int Add(int x, int y)
	{
		cout << "Add(int x, int y)" << endl;
		return x + y;
	}

	double Add(double x, double y)
	{
		cout << "Add(double x, double y)" << endl;
		return x + y;
	}


	//2.参数个数不同
	void func(int a)
	{
		cout << "func(int a)" << endl;
	}
	void func(int a, int b)
	{
		cout << "func(int a,int b)" << 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;
	}

}

int main()
{
	stn::Add(1, 2);
	stn::Add(1.1, 2.2);

	stn::func(1);
	stn::func(1,2);

	stn::f(1, 'x');
	stn::f('x', 1);


	return 0;
}

既然参数类型、参数个数、参数顺序都会构成函数重载,那么函数返回值是否会构成重载呢?

我们来试一下

#include <iostream>

using namespace std;

int f()
{
	cout << "f()" << endl;
	return 1;
}

//error C2556: “void f(void)”: 重载函数与“int f(void)”只是在返回类型上不同
void f()
{
	cout << "f()" << endl;
}


int main()
{

	f();
	f();

	return 0;
}

编译失败,编译器在调用时不知道到底调用哪一个函数!

最后我们来看下面这组代码

#include <iostream>

using namespace std;

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

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

int main()
{
	f();//error C2668: “f”: 对重载函数的调用不明确
	f(10);
	return 0;
}

首先来看,这两个函数是否构成函数重载?

答案是构成!

那为什么会报错呢?

当全缺省函数和无参函数都存在时,如果调用函数时没有进行传参,就会引发歧义,编译器不知道到底该调哪个函数!

3、inline

a、内联函数的定义

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

b、内联函数的使用

下面我们来写一个简单的内联函数

#include <iostream>

using namespace std;

inline int Add(int x, int y)
{
	int ret = x + y;

	ret += 1;

	return ret;
}

int main()
{
	int ret = Add(1, 2);
	cout << ret << endl;

	return 0;
}

通过调试来观察内联函数是否展开

发现框选部分并没有call/ret指令,即没有通过call指令进行调用,直接将代码复制过来!

最后要注意,inline不建议声明和定义分离到两个文件,分离会导致连接错误;

因为inline被展开,没有函数地址,链接时会出现报错!   

c、内联函数与宏函数的区别

C++设计内联函数的目的就是为了替代C语言的宏函数

C语言的宏函数也会在预处理时替换展开,但是宏函数很复杂,并且由于直接替换,不方便调试;

下面我们来简单实现一个加法宏函数

//请问哪个宏函数是正确的?
#define Add(int a,int b) return a + b;
#define Add(a,b) return a + b;
#define Add(a,b) (a + b)

答案是上述三个均是错误的宏函数

宏函数的本质是进行替换;

首先来看第一个

//#define Add(int a,int b) return a + b; 
int ret = Add(1, 2);  
//展开之后变成 int ret = return 1 + 2;;
//1.return不能出现在赋值表达式
//2.末尾有两个分号	

接着来看第二个

//#define Add(a,b) a + b;
int ret = Add(1, 2);	
//展开之后变成 int ret = 1 + 2;;
//此时影响不大,结果正常
cout << Add(1, 2) * 3 << endl;
//这样展开之后呢?
//cout << 1 + 2 * 3 ; << endl;
//显然1.没有括号 2.分号多余

继续来看第三个

//#define Add(a,b) (a + b)
cout << Add(1, 2) << endl; // -> cout << (1 + 2) << endl;
//此时没有问题
cout << Add(1 & 2, 1 | 2) << endl; // -> cout << (1 & 2 + 1 | 2) << endl;
//显然错误,+ 的运算符优先级高于 & ,导致计算顺序错误

最后来看正确写法

#define Add(a,b) ((a) + (b));

四、引用

1、引用的概念和定义

引用的基本概念

引用是C++中一种特殊的变量类型,本质上是已存在变量的别名;

通过引用可以直接操作原变量,无需拷贝数据;

引用在定义时必须初始化,且一旦绑定到某个变量后无法更改指向。

引用的定义语法

引用的定义方式为在变量类型后加 & 符号:

  • 类型匹配:引用必须与原变量类型一致(const 引用除外)。
  • 必须初始化:定义时需直接绑定到一个已存在的变量。
  • 无独立内存:引用不占用额外存储空间,仅作为别名存在。

我们尝试来使用引用

#include <iostream>

using namespace std;

int main()
{
	int a = 1;

	int& b = a;
	int& c = a;
	int& d = a;

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

	return 0;
}

2、const引用

a、引用const对象必须要用const来引用;const引用也可以引用普通对象,那么就会造成权限缩小的问题;

#include <iostream>

using namespace std;

int main()
{	
	const int a = 1;
	//权限放大:error C2440: “初始化”: 无法从“const int”转换为“int &”
	//int& ra = a;
	
	//正确引用
	const int& ra = a;
	
	//error C3892: “ra”: 不能给常量赋值
	//ra++;

	//权限缩小
	int b = 10;
	const int& rb = b;
	
	//error C3892: “rb”: 不能给常量赋值
	//rb++;

	return 0;
}

b、临时对象是指编译器需要一个空间暂存表达式的求职结果时,临时创建的一个未命名对象;

而在类型转换中就会产生临时对象,而临时对象具有常性,此时就需要使用常引用!

#include <iostream>

using namespace std;

int main()
{
	int a = 10;

	//常量值用常引用
	const int& x = 20;
	
	//error C2440: “初始化”: 无法从“int”转换为“int &”
	//int& ret = a * 2;

	//临时对象用常引用
	const int& ret = a * 2;

	double d = 11.22;
	
	//error C2440: “初始化”: 无法从“double”转换为“int &”
	//int& rd = d;

	//类型转换会产生临时对象,要用常引用
	const int& rd = d;


	return 0;
}

3、指针和引用

引用与指针的区别

  • 初始化要求:引用必须初始化,指针可以为空(nullptr
  • 语法概念上:引用是一个变量的别名,不开空间,指针是存储一个变量的地址,要开空间
  • 可修改性:引用绑定后不可更改,指针可以重新指向其他地址
  • 访问语法:引用直接使用变量名操作,指针需解引用(*ptr
  • 大小上:sizoef中引用的大小取决于引用类型的大小,但指针始终是地址空间所占字节个数
  • 安全性:引用不存在“空引用”问题,指针可能悬空

五、空指针

1、NULL

在C语言中,NULL实际上是一个宏;

在stddef.h文件中可以看到下面代码:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL    0
    #else
        #define NULL    ((void *)0)
    #endif
#endif

C++中NULL被定义成0,C语言中是一个类型为 void *、值为零的空指针常量;

我们来看下面代码

#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(int x)

	f((int*)0);
	f((int*)NULL);//调用f(int* ptr)

	//error C2665: “f”: 没有重载函数可以转换所有参数类型
	//f((void*)NULL);
	
	return 0;
}

会发现即使传入NULL,本想调用指针版本的,却仍调用整数版本!导致出现很多歧义;

最后由于C++禁止void*隐式类型转换为其他指针类型,因此会报错

2、nullptr

在C++中,为了避免函数调用歧义的问题,引入了新的关键字 nullptr;

nullptr可以转换成任意其他类型的指针;

因此,使用nullptr可以避免指针类型转换的问题;

nullptr只能隐式的转换成指针类型,而不能被转换成整数类型     

如果觉得有帮助,可以关注 GitHub 项目持续更新:GitHub - Stellen-z/DailyCode: pracetice · GitHub

更多推荐