C++入门

目录

1.命名空间

1.1命名空间的定义

1.2命名空间的使用

1.3std命名空间的使用惯例

2.缺省参数

2.1缺省参数的分类

2.2缺省参数注意事项

3.函数重载

3.1 函数重载概念

3.1.1构成函数重载必须是在同一作用域内

3.1.2函数重载和缺省参数

3.2 C++支持函数重载的原理

4.引用

4.1引用概念

4.2引用的使用场景

4.3引用的特性

4.4常引用

4.5引用和指针的区别

5.内联函数

5.1宏的优缺点

5.2概念

5.3特性

6.auto函数

6.1 auto简介

6.2auto使用规则

6.3 auto不能推导的场景

6.4范围for

7.指针空值nullptr


1.命名空间

        在写C++代码的时候,我们常常会看到一段代码

#include <iostream>
using namespace std;

        第一个很好理解,就是包了一个头文件,那么第二句的作用是什么呢,std是C++标准库的命名空间,C++所有标准库的都会放到std中,当我们写了这一句,就授权可以使用C++标准库函数

        在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

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

int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
    printf("%d\n", rand);    
    return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”

        这段代码在编译之后会报错,原因是<stdlib.h>中有一个函数也叫rand,而我们又重新定义了一个全局变量rand,就会产生歧异,后面我们使用rand,到底是库函数里面的rand还是我们自己定义的rand,所以C++就使用了命名空间来解决这个问题

1.1命名空间的定义

1.定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

namespace cola
{
    int rand = 0;
}

  2.命名空间中可以定义变量/函数/类型

namespce cola
{
    int rand = 10;

    int Add(int left, int right)
    {
        return left + right;
    }

    struct Node
    {
        struct Node* next;
        int val;
    };
}

  3.命名空间可以嵌套定义

namespace N1
{
    int a;
    int b;
    int Add(int left, int right)
    {
        return left + right;
    }

    namespace N2
    {
        int c;
        int d;
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}

  4.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。 

        一个工程中的test.h和上test.cpp中两个N1会被合并成一个

1.2命名空间的使用

1.命名空间名加上域作用限定符

namespace cola
{
    int rand = 0;
}

int main()
{
    printf("%p", rand);
    printf("%p", cola::rand);

    return 0;
}

        当使用rand的时候,默认会去全局找,如果我们想让他指定找命名空间里面的可以使用::(域作用限定符)。

2.使用using namespace 命名空间名称引入(全部展开/授权)

        我们可能会觉得每次都要加上::,可能有些麻烦,我们可以加上using namespce

#include <iostream>

namespace cola
{
	int rand = 0;

	int Add(int a, int b)
	{
		return a + b;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}

using namespace cola;

int main()
{
	//printf("%d", rand);  //错误
    printf("%d", cola::rand);
	printf("%d", Add(1, 2));
	struct Node s1;
}

        加上了这句话之后,相当于是全部展开了,其实也就相当于3命名空间已经失效了,所以第一句中的rand会产生歧义,不能分辨到底是哪一个,第二个rand已经指定了在命名空间里面去找,而第三句中的Add默认去全局找,没有找到,就会去命名空间里面找。(但是命名空间的全部展开是一种很危险的行为)

3.使用using将命名空间中某个成员引入(部分展开/授权)

#include <iostream>

namespace cola
{
	int rand = 0;

	int Add(int a, int b)
	{
		return a + b;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}

using cola::Add;

int main()
{
    printf("%d", rand);   //全局
	printf("%d", cola::rand);  //命名空间中的
	printf("%d", Add(1, 2));
	struct cola::Node s1;
}

        当我们需要大量使用一个命名空间中的成员时,就可以使用部分展开的形式,这样比较安全,也比较方便。例如这里的Add函数。

1.3std命名空间的使用惯例

1. 在日常练习中,直接using namespace std即可,这样就很方便。

2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。



2.缺省参数

        缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

void Func(int a = 0)
{
    cout << a << endl;
}

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

    return 0;
}

        当我们不传参数的时候,例如第一个Func(),则调用函数时,a就是给的缺省值,也就是0,如果给了参数的时候,例如第二个,那么a就是10了。

2.1缺省参数的分类

1.全缺省

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

        函数的每个参数都有缺省值。注意:在调用的时候,必须从左往右显示传参

int main()
{
    //正确写法
    Func();
    Func(1);
    Func(1,2);
    Func(1,2,3);

    //错误写法
    //Func(, 1, );
    //Func(, , 1);
    //Func(1, , 3);
}

2.半缺省

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


int main()
{
    Func();
    Func(1);
    Func(1,2);
    Func(1,2,3);
}

        半缺省参数必须从右往左依次来给出,不能间隔着给。例如以下错误写法

//错误写法
void Func(int a = 10, int b = 10, int c)
{
    cout < <"a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

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

2.2缺省参数注意事项

1. 半缺省参数必须从右往左依次来给出,不能间隔着给

2. 缺省参数不能在函数声明和定义中同时出现,最好声明给,定义的时候不给。   注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值

//a.h
void Func(int a = 10);

// a.cpp
void Func(int a = 20)
{}

3. 缺省值必须是常量或者全局变量

4. C语言不支持(编译器不支持)



3.函数重载

3.1 函数重载概念

        函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

#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(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;
}


int main()
{
    Add(10, 20);
    Add(10.1, 20.2);
    f();
    f(10);
    f(10, 'a');
    f('a', 10);

    return 0;
}

        调用函数时,会自动匹配类型,例如main函数中第一个Add,两个参数都是int类型,就会自动去匹配Add函数中两个参数是int类型的那一个Add函数,所以第一个Add调用的就是第一个Add函数,第二个Add调用的就是第二个Add函数。

 3.1.1构成函数重载必须是在同一作用域内

namespace s1
{
    int func(int a)
    {
        
    }
}

namespace s2
{
    int func(int a, int b)
    {
        
    }
}

所以这两个函数是不构成函数重载的。

3.1.2函数重载和缺省参数

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

void Func(int a, int b = 10)
{
	cout << "void Func(int a)" << endl;
}

        这两个函数是构成函数重载的, 但是在调用的时候,就会产生歧义,如果我们调用Func(2)的话,编译器就不知道我们调用的到底是哪一个函数

3.2 C++支持函数重载的原理

C++支持函数重载的原理--函数名字修饰(name Mangling)

为什么C++支持函数重载,而C语言不支持函数重载呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

在这过程中,会生成一个符号表,符号表里面是函数名和他地址的映射,当我们需要找函数地址的时候,就会通过符号表里面的名字去找地址。C语言是直接用函数名去充当符号表中的名字

如果此时有两个func,那我们去符号表中找的时候,都不知道去调哪一个地址的func。而C++会对函数名进行修饰之后再放入符号表中

比如这里红方框内的就是经过函数名修饰之后的函数名,本质上是把函数的参数也加入了函数名当中,所以只要参数的类型,数量,顺序不同,在符号表中的函数名就不同。调用的时候就能区分到底是哪一个函数了,所以C++支持函数重载

        函数的地址是第一条语句的地址,所以当我们只写声明不写定义的时候,这个函数是没有地址的,如果我们要调用这个函数,会根据函数名去找函数地址,但是符号表中是找不到的,也就会出现链接错误。

        如果参数相同,返回值不同的同名函数是不能构成重载的,因为在调用的时候,还是会不知道到底要调用哪一个。



4.引用

4.1引用概念

        引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。(注意:引用类型必须和引用实体是同种类型的

void TestRef()
{
	int a = 10;
	int& ra = a;//<====定义引用类型

	printf("%p\n", &a);
	printf("%p\n", &ra);
    
    a++;
    ra++;
}

        引用就是给变量取别名,给重新取了一个名字叫ra,所以ra和a现在指向的就是同一块空间,当我们对a操作,会影响ra的结果,对ra操作,也会影响a的结果。

4.2引用的使用场景

1. 做参数(输出型参数)

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

int main()
{
    int a = 10;
    int b = 20;
    Swap(a, b);
}

        在C语言中,我们想写一个交换函数,必须要传地址,然后进行解引用访问,虽然也可以用,但是C++引用,会更加方便,可读性也更高。

2.做返回值

1.传值返回

int Add(int a, int b)
{
    int n = 0;
    n++;

    return n;
}

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

	return 0;
}

        对于Add函数,出了作用域n将会销毁,在Add函数结束之后,会创建一个临时变量,然后会把n的值拷贝给这个临时变量,之后n再会销毁。所以返回的就是n的值

 2.传引用返回

int& Add(int a, int b)
{
    int n = 0;
    n++;

    return n;
}

int main()
{
	int& ret = Add(1, 2);
	
    return 0;
}

        传引用返回,则返回的是n的别名,相当于是将n传回去,但是出了作用域n就会销毁,所以返回值不能确定,取决于编译器会不会把销毁的空间置为随机值,如果不会置为随机值,那么返回的就是1。

int& Add(int a, int b)
{
    int n = 0;
    n++;

    return n;
}

int main()
{
	int& ret = Add(1, 2);
    cout << ret << endl;
    cout << ret << endl;
	
    return 0;
}

        如果ret类型为引用,并且返回的也是n的别名,所以ret也是n的别名,ret指向的就是被销毁的那个空间,来看看运行结果。

        n离开Add函数后,返回了n的别名,但是编译器没有将n的那片空间置为随机值,而ret指向的就是那片空间,而当我们调用cout函数之后,会建立栈帧,此时的栈帧就覆盖了之前n的那片空间,调用第一个cout函数,会先传将ret的值传给cout,也就是将1传给了cout,传完参数之后再建立栈帧,此时n位置的值已经被置为随机值了,当我们第二次调用cout函数,就会把随机值传给cout。

再来举一个例子:

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;

	return 0;
}

         最后运行的结果是7,因为,第一次调用Add函数之后,c为3,并且把c的别名返回回去,ret就是c的别名,第二次调用Add函数后,c的值被修改成了7,ret是c的别名,所以ret的值也为7。

总结:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

传引用传参(任何时候都可以)

1.提高效率(传的就是实参别名,不用额外拷贝)

2.做输出型参数(形参的改变会影响实参)

传引用返回(出了函数作用域对象还在)

1.提高效率

2.修改返回对象

4.3引用的特性

1. 引用在定义时必须初始化

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

3. 引用一旦引用一个实体,再不能引用其他实体

void TestRef()
{
	int a = 10;
	// int& ra; // 该条语句编译时会出错
	int& ra = a;
	int& rb = a;
	int& rra = ra;

    int b = 20;
    ra = b;  //这里是赋值,而不是更换指向
}

这几个变量指向的都是同一个空间。

4.4常引用

void TestConstRef()
{
	const int a = 10;
	//int& ra = a; // 该语句编译时会出错,a为常量
}

a是一个常量,如果直接传给ra是不可以的,这相当于是一种权限的放大,这是不合适的,a做不到的事情,作为a的别名,ra也是做不到的

void TestConstRef()
{
	const int a = 10;
    const int& ra = a;	
}

这是权限的平移,是正确的。

void TestConstRef()
{
	int a = 10;
    const int& ra = a;
}

这是权限的缩小,权限只能平移和缩小,不能放大

int i = 10;
//double& d = i;  //错误写法,是一种权限的放大
const double& d = i;

赋值的时候,是先将i赋值给一个临时变量,临时变量是一个double类型的常量,再将常量赋值给double类型,会发生权限的放大,所以加上一个const之后才是正确的。

4.5引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。引用和指针在汇编代码下其实是一样的。

引用和指针的不同点:

1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

2. 引用在定义时必须初始化,指针没有要求

3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

4. 没有NULL引用,但有NULL指针

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

6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

7. 有多级指针,但是没有多级引用

8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

9. 引用比指针使用起来相对更安全



5.内联函数

        在c语言中吗,如果我们想写一个宏函数,其实是比较麻烦的,例如Add函数要写成

#define Add(x, y) ((x) + (y))

稍不注意可能就会写错,而且宏不支持调试。所以C++为了补这个坑,加入了内敛函数的概念。

5.1宏的优缺点

优点:

1.增强代码的复用性。

2.提高性能。

缺点:

1.不方便调试宏。(因为预编译阶段进行了替换)

2.导致代码可读性差,可维护性差,容易误用。

3.没有类型安全的检查 。

5.2概念

        以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

当没有加inline的时候,会发生函数的调用(call语句)。 如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。

加了inline之后,直接在调用的地方展开了,就不用去调用Add函数了,也不用去开辟额外的栈帧,加快了速度。

5.3特性

1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率

2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

 3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。(内联函数在符号表中没有地址,因为函数要在调用的地方展开,没有必要去生成一堆指令)。


6.auto函数

6.1 auto简介

        使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

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

编译器就会自动推导出b为int类型,c为int*类型,d为引用类型

普通场景下其实是没有什么意义的,如果类型很长的情况下,就很方便了

int main()
{
	std::map<std::string, std::string> m;
	//std::map<std::string, std::string>::iterator it = m.begin();
	auto id = m.begin();

	return 0;
}

使用auto就可以简化代码。

6.2auto使用规则

1. auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;

2. 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同

6.3 auto不能推导的场景

1. auto不能作为函数的参数
 

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2. auto不能直接用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}

6.4范围for

        for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };

    for(auto& e : array)
        e *= 2;

    for(auto e : array)
        cout << e << " ";

    return 0;
}

        使用范围for会依次取array数组中的元素,赋值给e,一般搭配auto来使用,auto可以自动识别类型。范围for会自动判定结束,自动迭代。



7.指针空值nullptr

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

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

        NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。

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

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

int main()
{
    f(0);
    f(NULL);
    f((int*)NULL);

    return 0;
}

调用f(NULL)的时候,其实会走第一个函数,NULL代表的值为0而不是一个指针,所以在C++中,修改了这个坑,加入了nullptr作为空指针。所以在C++中表示空指针最好使用nullptr

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值