一. 标准输入输出

输入输出
C++ 使用 iostream 库进行输入输出,要包含头文件,其包含两个基础类型istream与ostream,分别代表输入流与输出流,与 C 语言的 printf 和 scanf 不同,C++ 使用的是流 (Stream) 的概念。

cout:标准输出流(通常是屏幕),为ostream类型对象

cin:  标准输入流(通常是键盘),为istream类型对象

<<:流插入运算符(用于输出)。

>>:流提取运算符(用于输入)。

例如:

#include <iostream>

int main()
 {
    int age;
    std::cout << "请输入您的年龄: "; 
    std::cin >> age; // 从键盘读取数据赋值给 age
    std::cout << "您今年 " << age << " 岁。" << std::endl; 
    return 0;
}

解释:前缀std::指出cin,cout,endl是定义在名为std的命名空间中的,而::作用域运算符,来指出我们想使用定义在std中的名字。

注意:这⾥我们没有包含,也可以使⽤printf和scanf,在包含间接包含了。vs系列编译器是这样的,其他编译器可能会报错。

易错点
方向写反:新手常把 cin >> 写成 cin <<。记住:箭头指向数据流动的方向(cin 的数据流向变量,变量的数据流向 cout)。

缺少类型安全性:虽然 cin 会自动识别类型,但如果用户输入了非法字符(如要求输入整数却输入了字母),cin 会进入错误状态,后续输入将失效。

二. 命名空间

1.namespace的价值

这里可能会有人有疑问,为什么需要这个命名空间?下面以一个示例来演示一下:

在Test.cpp中,

#include <stdio.h>                                                             
//#include <stdlib.h>
int rand = 10;
int main()
{
    printf("%d\n", rand);
    return 0;
}

我们运行时,发现这个可以正常的运行,倘若将这个头文件的注释删除了呢?再次运行,就会发现一下的报错

原因:全局变量 rand 与标准库函数 rand() 重名,在C语言中,函数名和普通变量名位于同一个标识符命名空间。同一作用域下,rand 不能既表示函数又表示变量。

所以C++引⼊namespace就是为了更好的解决这样的命名冲突问题。

2.namespace的定义

2.1.使用

有一下的规则:

<1>.定义命名空间,需要使⽤到namespace关键字后面跟命名空间的名字,然后接⼀对{}即可,{}中 即为命名空间的成员。命名空间中可以定义变量/函数/类型等。

例如:

namespace bit
{
	int rand = 10;
}

<2>.namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下面的rand不在冲突了,如代码所示:

#include<iostream>

using namespace std;
namespace kong
{
	int rand = 10;
}
int main()
{
	cout << rand << endl;
	return 0;
}

运行结果:

为什么不是我们想要的结果为10的rand,却是这个16进制的随机值呢?

原因:rand 被放在 kong 命名空间中,而 main 位于全局作用域,编译器不会主动进入 kong 命名空间查找。因此,cout << rand 找到的是标准库中的 rand 函数。由于没有添加 (),函数并未被调用,输出的是函数地址。命名空间中的成员通常需要通过“命名空间名 + ::”访问,例如 kong::rand

我们向这个命名空间kong里面添加一些内容,方便演示:

namespace kong
{
	int rand = 10;
	int Add(int left, int right)
	{
		return left + right;
	}
}
int main()
{
	cout << kong::Add(1, 2) << endl;
	cout << kong::rand << endl;
	return 0;
}

运行结果:

<3>.namespace只能定义在全局,当然他还可以嵌套定义,例如:

namespace kong
{
	namespace shao
	{
		int rand = 1;
		int Add(int left, int right)
		{
			return left + right;
		}
	}
	namespace ou
	{
		int rand = 2;
		int Add(int left, int right)
		{
			return (left + right) * 10;
		}
	}
}
int main()
{
	cout << kong::rand << endl;
	cout << kong::shao::rand << endl;
	cout << kong::ou::rand << endl;
}

运行结果:

<4>.项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。

注意:C++标准库都放在⼀个叫std(standard)的命名空间中,在项目使用中最好不要展开std,至于什么是展开命名空间下面会讲。

2.2.命名空间的展开

通过上面的例子演示我们会发现一个问题:明明cout和endl都在std命名空间里面啊,但是在使用时却没有用到::,这就要提到命名空间的展开了。

<1>.指定命名空间访问,项⽬中推荐这种方式。

#include <iostream>

namespace my_space 
{
    int a = 10;
    void print() 
   { 
      std::cout << "From my_space" << std::endl; 
   }
}

int main()
 {
    // 显式指定命名空间访问
    std::cout << my_space::a << std::endl;
    my_space::print();
    return 0;
}

但是这样使用的话,我们就要不断的使用std::了。

<2>.using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种方式。

#include <iostream>

namespace my_space
 {
    int a = 10;
    int b = 20;
    void print() 
    { 
       std::cout << "From my_space" << std::endl; 
    }
}

// 仅展开需要的成员
using my_space::a;
using std::cout;        //展开std中的cout,但是使用endl时还需要加std::的

int main()
 {
    // 直接使用a和cout
    cout << a << std::endl;
    // 未展开的成员需要显式访问
    cout << my_space::b << std::endl;
    my_space::print();
    return 0;
}

<3>.展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。

#include <iostream>
//全部展开std
using namespace std;

namespace my_space 
{
    int a = 10;
    void print() 
    { 
       cout << "From my_space" << endl; 
    }
}

// 全部展开 my_space
using namespace my_space;

int main() {
    // 直接使用,无需 my_space::
    cout << a << endl;
    print();
    return 0;
}

注意:using namespace std在小型练习中没问题,但在大型项目中,如果你的变量名恰好和标准库里的某个函数同名,会导致难以察觉的编译错误。

三.缺省参数

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

使用规则:

<1>.在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。

例如:

#include <iostream>
using namespace std;

// 全缺省:三个参数都给了默认值
void func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}

int main()
{
    func();          // 不传参:a=10, b=20, c=30
    func(1);         // 传1个:a=1, b=20, c=30
    func(1, 2);      // 传2个:a=1, b=2, c=30
    func(1, 2, 3);   // 传3个:a=1, b=2, c=3
    return 0;
}

运行结果:

但是像下面这样写就是不行的:

func(1, , 2);

<2>.全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。

例如:

#include <iostream>
using namespace std;

// 半缺省:b和c给默认值,a不给 → 合法(从右往左连续)
void func(int a, int b = 20, int c = 30)
{
    cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}

// ❌ 错误示例:a给默认值,b不给,c给 → 不合法(不是从右往左连续)
// void func(int a = 10, int b, int c = 30) { ... }

int main()
{
    func(1);         // 传1个:a=1, b=20, c=30
    func(1, 2);      // 传2个:a=1, b=2, c=30
    func(1, 2, 3);   // 传3个:a=1, b=2, c=3
    // func();       // ❌ 错误:a没有默认值,必须传参
    return 0;
}

运行结果:

<3>.带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。

例如:

#include <iostream>
using namespace std;

void func(int a, int b = 20, int c = 30)
{
    cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}

int main()
{
    func(1);              // ✅ 合法:传a,b和c用默认
    func(1, 2);           // ✅ 合法:传a、b,c用默认
    // func(1, , 3);      // ❌ 错误:不能跳过b直接传c
    return 0;
}

运行结果:

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

例如:

// 在test.h中声明:缺省参数写在这里
#include <iostream>
using namespace std;
void func(int a = 10, int b = 20);

// 在test.cpp中定义:不能再写缺省参数
void func(int a, int b)
{
    cout << "a = " << a << ", b = " << b << endl;
}

//这个是main.cpp中的代码
#include"test.h"
int main()
{
    func();       // ✅ 合法:a=10, b=20
    func(5);      // ✅ 合法:a=5, b=20
    return 0;
}

四. 引用

1.使用

在 C++ 的引用语法里, & 引用声明符,它的作用是告诉编译器:当前定义的变量是一个“引用”(即另一个变量的别名),与原变量共处一块空间。

语法:类型&引用名=原变量;

例如:

#include <iostream>
using namespace std; 
int main()
{
	int a=10;
    int&ra=a;
    int&rb=ra;
    int&rc=rb;
    cout<<a<<' '<<ra<<' '<<rb<<' '<<rc<<endl; 
	return 0;
}

运行结果:

上面的ra,rb,rc均为a的别名。

作用:可以用于引用传参和引用作返回值,来减少拷贝,提高效率。

2.特点

<1>.定义时必须初始化

int a = 10;
int& ref = a; //正确:定义时初始化
// int& ref; //错误:未初始化,编译报错

<2>.一个变量可以有多个引用

int a = 10;
int& ref1 = a;
int& ref2 = a; //正确:a有两个引用ref1、ref2

ref1 = 20;
cout << a << " " << ref2 << endl; // 输出 20 20

<3>.引用一旦引用一个实体,就不能引用其它的实体

int a = 10;
int b = 20;
int& ref = a; // 绑定a
ref = b;      // 不是绑定b,而是把b的值赋给ref(即 a = 20)
cout << a << endl; // 输出 20
cout << &ref << " " << &a << endl; // 地址相同,仍绑定a

3.引用的使用

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

例如交换函数中的参数就可以用引用:

#include <iostream>
using namespace std;
void Swap(int& rx, int& ry)
{
    int tmp = rx;
    rx = ry;
    ry = tmp;
}

int main()
{
    int x = 0, y = 1;

    cout << x << " " << y << endl;

    Swap(x, y);

    cout << x << " " << y << endl;

    return 0;
}

函数返回的时候也可以用引用,下面以一个简单的例子来举例:

#include <iostream>
#include <assert.h>

using namespace std;

typedef struct Stack
{
    int* a;
    int top;
    int capacity;
} ST;

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

// 栈顶,为了方便,就不考虑扩容了,毕竟仅插入几个数据而已
void STPush(ST& rs,int x)
{
    rs.a[rs.top] = x;
    rs.top++;
}

// int STTop(ST& rs)
int STTop(ST& rs)
{
    assert(rs.top > 0);

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

int main()
{
    // 调用全局的
    ST st1;

    STInit(st1);

    STPush(st1, 1);
    STPush(st1, 2);
    int x = STTop(st1);
    STTop(st1) += 10;

    cout << STTop(st1) << endl;
    cout << x << endl;

    return 0;
}

运行结果:

原因:我们这个是传值返回,返回时会把数组元素复制一份,STTop(st1)得到的不是 a[1] 本身,而是它的一份临时对象,属于右值,+= 需要修改左边的对象,所以它的左操作数必须是一个“可修改的左值”,所以此时我们可以将STTop修改成:

// int STTop(ST& rs)
int& STTop(ST& rs)
{
    assert(rs.top > 0);

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

运行结果:

4.const引用

特点:
<1>.可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小但是不能放大

#include <iostream>
using namespace std;

int main()
 {
    int a = 10;
    // int& rb = a * 3; //编译报错:无法绑定临时对象到非const引用
    const int& rb = a * 3;//合法:const引用接受带常性的临时对象
    cout << rb << endl; // 输出30,临时对象生命周期随const引用延长
    return 0;
}

<2>.不过需要注意的是类似int& rb=a*3;double d=12.34;int& rd=d;这样一些场景下a*3的和结果保存在一个临时对象中,int&rd=d也是类似,在类型转换中会产生临时对象存储中间值,也就是,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。

#include <iostream>
using namespace std;

int main() 
{
    double d = 12.34;
    // int& rd = d;//编译报错:double转int生成临时对象,非const引用不能绑
    const int& rd = d;//合法:const引用绑定带常性的临时对象
    cout << rd << endl;// 输出12,取临时对象的值(类型转换后结果)
    return 0;
}

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

注意:

#include <iostream>
using namespace std;

int main() 
{
    int a = 20; // 普通对象:可读可写
    const int& ca = a;//合法:权限从可写→只读,缩小允许
    cout << ca<< endl; //输出20
    a = 30; // 原对象仍可改,ca会同步变(共享内存)
    cout << ca << endl;//输出30 ✅
    return 0;
}

总结:所有生成临时对象的场景(表达式计算、类型转换),临时对象都有常性,只能用const引用绑定;const引用绑普通对象,本质是主动缩小访问权限,完全合法。

5.指针与引用的区别

• 语法概念上引用是⼀个变量的取别名,不开空间,指针是存储⼀个变量地址,要开空间

• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

• 引用在初始化时引用⼀个对象后,就不能再引用其他对象;⽽指针可以在不断地改变指向对象。 • 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。

• sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)

• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引用使用起来相对更安全⼀些

五.函数重载

特点:在C++中, 允许你使用同一个函数名来定义多个功能相似但参数不同的函数,在C语言中,如果想写一个加法函数,可能会遇到这样的情况,处理整数:add_int(int a, int b),处理浮点数:add_float(float a, float b),处理三个整数:add_three_ints(int a, int b, int c),但在C++中一个方程式就可以表示这三个。

核心定义:指在同一个作用域内,可以有一组函数名相同,但参数列表不同的函数。

要构成重载,函数的参数列表必须满足以下至少一个条件不同:

<1>.参数类型不同void func(int a)void func(double a)。

例如:

#include <iostream>
using namespace std;
// 版本 1:处理两个整数
int add(int a, int b) 
{
    return a + b;
}
// 版本 2:处理两个浮点数
double add(double a, double b)
{
    return a + b;
}
int main() 
{
    // 编译器会根据你传的参数类型,自动“点菜”
    cout << "1 + 2 = " << add(1, 2) << endl;         // 匹配版本 1
    cout << "1.5 + 2.3 = " << add(1.5, 2.3) << endl; // 匹配版本 2
    return 0;
}

运行结果:

<2>.参数个数不同void func(int a)void func(int a, int b)。

例如:

#include<iostream>
using namespace std;
int fun1(int x,int y)
{
    return x+y;
}
int fun1(int x,int y,int z)
{
    return x+y+z;
}
int main()
{
    int ret1=fun1(4,6);
    int ret2=fun1(4,5,6);
    cout<<"ret1="<<ret1<<endl;
    cout<<"ret2="<<ret2<<endl;
    return 0;
}

运行结果:

<3>.参数顺序不同void func(int a, double b)void func(double a, int b)。

#include <iostream>
using namespace std;
// 版本 A:顺序是 (字符, 整数)
void show(char level, int id) 
{
    cout << "等级是: " << level << ", 编号是: " << id <<endl;
}
// 版本 B:顺序是 (整数, 字符) —— 顺序换了,构成了重载
void show(int id, char level)
{
    cout << "编号是: " << id << ", 等级是: " << level << endl;
}
int main() 
{
    // 编译器根据你传参数的顺序来自动选择
    show('A', 101); // 匹配 (char, int) -> 调用版本 A
    show(102, 'B'); // 匹配 (int, char) -> 调用版本 B
    return 0;
}

运行结果:

注意1:只有返回值类型不同是无法构成函数重载的。

例如:

#include <iostream>
// 函数 1:返回整数
int getData(int x) 
{
    return x;
}
// 函数 2:返回小数
// ❌ 报错!编译器会提示“无法重载仅按返回类型区分的函数”
double getData(int x) 
{
    return (double)x;
}
int main() 
{
    //这样调用,编译器该选哪一个呢?
    getData(10); 
    return 0;
}

运行结果:

注意2:当重载函数中的参数设置了默认值时,要注意它是否会与其他重载函数形成相同的调用方式,例如:

#include<iostream> 

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

int main()
{
	f1();
	return 0;
}

运行结果:

原因:编译器无法判断应该调用哪一个,因此产生函数调用二义性错误。

六.nullptr关键字

在C语言中NULL完整定义形式为:

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

在 C 语言中void* 可以自动转换为任何类型的指针,所以用 ((void *)0) 非常安全且确, C++ 语言中:C++ 具有更严格的类型检查,不允许 void* 隐式转换成其他指针(比如 int*)。如果 C++ 也用 ((void *)0),那么 int* p = NULL; 就会报错。因此,C++ 只能折中把 NULL 定义为整数 0

例如在C++中有这样的代码:

#include<iostream>
using namespace std;
void fun(int x)
{
	cout<<"fun1(x)"<<endl;
} 
void fun(int*ptr)
{
	cout<<"fun2(ptr)"<<endl;
}
int main()
{
	fun(0);//调用第一个函数 
	fun(NULL); //在C++中NULL会被确认为0,所以编译器可能会报错,或者调用第一个函数 
	return 0;
}

运行结果:

nullptr关键字是一个指针字面量,属于一种特殊的类型 std::nullptr_t,它不是整数,但可以安全地转换为任何类型的指针,而不是整数类型。

此时上面的代码可以改为:

#include<iostream>
using namespace std;
void fun(int x)
{
	cout<<"fun1(x)"<<endl;
} 
void fun(int*ptr)
{
	cout<<"fun2(ptr)"<<endl;
}
int main()
{
	fun(0);//调用第一个函数 
	fun(nullptr);//调用第二个函数
	return 0;
}

运行结果:

七.inline函数

在 C 语言/C++中,宏(#define)本质上是一种简单的文本替换机制。然而,如果使用不当,很容易在运算时产生意想不到的错误。

例如:

#include <iostream>
#define SQUARE(x) x * x
int main() 
{
    // 期望的结果是 (1+2) * (1+2) = 9
    // 实际替换结果:1 + 2 * 1 + 2 = 5
    int result = SQUARE(1 + 2); 
    cout << result << endl;
    return 0;
}

运行结果:

修正方案: 给参数和结果加括号,修改成 #define SQUARE(x) ((x) * (x))。

但是就算给参数和结果加括号,也可能会出错,例如:

#include <iostream>
using namespace std;
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() 
{
    int i = 5, j = 3;
    // 你期望 i 变成 6,结果返回 6
    // 实际替换结果:((++i) > (j) ? (++i) : (j))
    int res = MAX(++i, j); 
    // 此时 i 竟然变成了 7!因为它在比较时加了一次,在返回结果时又加了一次。
    return 0;
}

运行结果:

针对以上的错误,inline函数就可以解决。

vs编译器debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置⼀下 以下两个地⽅,如图:

内联函数语法形式:

inline 返回类型 函数名(参数列表) 
{
    // 函数体
    ...
    return 返回值;
}

特点:

<1>.编译器在编译阶段将调用处的函数名直接替换为完整的函数体代码。

<2>.由于没有了函数调用的参数传递、栈帧建立和恢复、跳转等开销,程序运行速度更快。

<3>.内联函数是函数,编译器会对参数类型返回值进行严格检查,避免了宏替换中常见的逻辑漏洞。

<4>.inline 只是给编译器提个建议。编译器会根据函数的大小、复杂度以及自身的优化策略来决定是否真正内联,函数体包含复杂whilefor 循环、switch 语句或递归调用,编译器通常会拒绝内联。

<5>.inline不建议定义与声明分离到两个文件,分离会导致错误,通常定义在 .h 文件中。

例子:

#include <iostream>
using namespace std;
// 将宏改为内联函数
int square(int x) 
{
    return x * x;
}
int main() 
{
    int a = 5;
    // 此时调用 square(++a)
    int result = square(++a); //相当与(++a)*(++a),即6*6
    std::cout << "result = " << result << std::endl; // 输出 36
    std::cout << "a = " << a << std::endl;           // 输出 6
    
    return 0;
}

没有inline时,我们点击F10,然后单击右键转到反汇编,如图:

我们调用函数都是要根据函数地址进行跳转的,但是加上inline时,如图:

内联后,编译器会把函数体直接展开到调用位置

缺点:

<1>.如果一个内联函数比较大,且在程序中被调用了成百上千次,最终编译出来的可执行文件体积会显著增大。

<2>.如果函数太复杂(有循环、递归、或是代码量太多),编译器会为了保护程序性能而自动取消内联

例如:

inline int square(int x)
{
    x++;
    x++;
    x++;
    x++;
    x++;
    x++;
    x++;
    x++;
    x++;
    return x * x;
}

如图:

所以inline函数应用于函数调用次数较少,函数内部不复杂,函数体较小的情况

下一篇为类与对象(上)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐