C++初阶1

目录

介绍:

一,命名空间

1-1,命名空间的定义

1-2,命名空间的使用

1-3,C++标准官方命名空间

二,缺省参数

2.1,缺省参数分类

三,函数重载

四,引用

4-1,引用的使用

4-2,常引用

4-3,引用的使用场景

4-4,引用的效率分析以及与指针的关系

五,内联函数


介绍:

        本文开始从C到C++的过度,C++是C的进阶版,在C中,本身存在很多不足和不安全性,C++是进一步弥补了C的不足并提供了更高级的用法,不仅如此,大部分C++编译器还支持C语言的用法,但C语言不支持C++的用法,这样一来C++即包括了C语言的优点,还拥有了自己的独特优势,在后面的写入中笔者会将两者语言掺和起来,以方便学者们理解。在平常区分C语言和C++我们可直接根据两者的后缀名,C语言的基础文件都是以".c"为后缀,而C++的基础文件都以".cpp"为后缀。

一,命名空间

        在C/C++中,变量、函数和以后我们要学习的类都是大量存在的,但如果变量或函数名重复了又当会如何?C语言本身是无法解决此问题,一旦重复系统将报错,而C++将会使用命名空间解决此问题。在C++中专门有命名空间关键字namespace

#include
int rand = 0;
int main() {
    //系统将会报错,因为在C语言库文件中就已定义了rand函数,不可再次进行定义
    fprintf(stdout, "rand: %d", rand);
    return 0;
}

1-1,命名空间的定义

        定义:namespace 命名空间的名字,然后下面接一堆{}即可,{}中即为空间中的成员。

        解析:命名空间相当于定义了一个新的作用域,该空间中的所有内容都局限于该空间中,不被外面影响,因此,它完美的解决了名称相同的问题。当要使用此空间中的成员时,需要用到域的作用限定符,即"::"。使用方法:命名空间名字::成员。要注意的是,使用结构体成员时:struct 命名空间的名字::结构体名称,并且,命名空间还可以嵌套使用。

#include
//str是命名空间的名字
namespace str {
    //命名空间里可以是任意类型
    int rand = 0;
    int Add(int a, int b) {
        return a + b;
    }
    struct student {
        int age;
        int height;
    };

    //命名空间的嵌套使用
    namespace bitee {
        int a = 6;
    }
}
int main() {
    //整型类型的运用,其它类型同理
    fprintf(stdout, "rand = %d\n", str::rand);
    //结构体类型的运用
    struct str::student a1;
    a1.age = 2;
    fprintf(stdout, "结构体的运用: %d\n", a1.age);
    //函数的运用
    fprintf(stdout, "Add(1, 2) = %d\n", str::Add(1, 2));
    //命名空间的嵌套使用
    fprintf(stdout, "嵌套使用: %d\n", str::bitee::a);
    return 0;
}

        注意:在这里要说明的是C++允许同一个工程中存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中,但同一个文件下不允许存在相同名称的命名空间,因为这样运用的话当两者同时定义相同的变量名时编译器将"不知所措"。例如:一个工程中的test.h和test.cpp文件中都有namespace std,运行时两个的std会被合成一个。


1-2,命名空间的使用

        命名空间的使用有三种方法,其中,以上运用域的限定符"::"只是其中最普通的一种。接下来我们观察以下两种。

        1,使用using将命名空间中某个成员引入

#include
//str是命名空间的名字
namespace str {
    //命名空间里可以是任意类型
    int rand = 0;
    int Add(int a, int b) {
        return a + b;
    }
    struct student {
        int age;
        int height;
    };
    namespace bitee {
        int a = 6;
    }
}

//使用using后系统直接默认使用,这时不用域的限定符即可表示使用里面的数据
using str::rand;
using str::bitee::a;
int main() {
    fprintf(stdout, "%d %d\n", rand, a);
    return 0;
}

        2,使用using namespace 命名空间名称 将其全部引入

//将命名空间全部导入系统,这时会默认使用str
using namespace str;
int main() {
    fprintf(stdout, "%d %d\n", rand, bitee::a);
    return 0;
}


1-3,C++标准官方命名空间

        之前就说过,C++是C的进阶,C++高级功能的其中之一就体现在C++有自己的官方命名空间std,C++将标准库的定义实现都放到这个命名空间中,而std的使用通常与C++的里标准库文件密不可分,两者一般是配合使用。

C++常用标准函数:

        1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件 以及按命名空间使用方法使用std。

        2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。

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

        4. 使用C++输入输出更方便,不需要像C中printf/scanf输入输出时那样,需要手动控制格式。 C++的标准输入输出可以自动识别变量类型。

注意:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,笔者会详细跟大家介绍。

//引用头文件

#include
//标准命名空间的定义
using namespace std;
int main() {
    int a = 5;
    float b = 3.5;
    char str[] = "abcdf";
    cout << "a = " << a << endl << "b = " << b << endl << "str = " << str << endl;
    return 0;
}


二,缺省参数

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

#include
using namespace std;
void Fun(int a = 5) {
    cout << "a = " << a << endl;
}
int main() {
    Fun(); //a = 5,采用该函数的缺省值5
    Fun(10);//a = 10,有了实参就用该实参的值10
    return 0;
}

2.1,缺省参数分类

1,全缺省参数

        全缺省参数是函数参数中全部指定缺省值,如下:

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

2,半缺省参数

        半缺省参数是函数参数中部分指定缺省值,但这里有两条注意事项:1,半缺省参数必须从右往左依次来给出,不能间隔着给。2. 缺省参数不能在函数声明和定义中同时出现如下:

//参数从右到左必须依次给出

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

//错误运用

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

//缺省参数的使用注意

#include
using namespace std;
//函数定义和声明中不能同时给缺省值,因为编译器无法确定用哪个缺省值
//缺省值最好在声明中指定,因为我们运用函数一般都是先声明完之后在定义,如果系统先查看声明,里面没有缺省参数而定义中存在缺省参数将会报错

void fund(int a = 5) {
    cout << a << endl;
}
int main() {
    void fund(int a);
    fund();//将会报错,因为运行到函数声明时里面没有缺省参数而定义中指定缺省参数
    return 0;
}

        当出现缺省参数时,函数传参也必须要从右到左依次给出,如下:

#include
using namespace std;
void Func(int a = 10, int b = 20, int c = 30) {
    cout << a << endl;
    cout << b << endl;
    cout << c << endl;
}
int main() {
    Func();//a = 10,b = 20,c = 30
    Func(2);//a = 2,b = 20,c = 30
    Func(1, 2);//a = 1,b = 2, c = 30
    Func(1, 2, 3);//a = 1,b = 2, c = 3
    Func(, 3, );//错误用法
    return 0;
}


三,函数重载

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

        函数重载的注意要点其实主要根据编译器的识别。当函数名相同时,只要当我们运用重名函数时能够区分两者的不同即可通过。如当传参时参数的类型不同或个数不同或顺序不同。如下:

例一:

#include
using namespace std;
int Func(int a = 10, int b = 20, int c = 30) {
    return a + b + c;
}
int Func(int a = 1, int b = 2) {
    return a + b;
}
int main() {
    Func();//系统将报错,因为此时两个重名函数都满足条件,编译器无法区分用哪个
    Func(2);//系统报错,同理
    Func(1, 2);//系统报错,同理
    Func(1, 2, 3);//参数数量不同,可以区分,正常运行
    return 0;
}

例二:

#include
using namespace std;
void Func(int a = 10, int b = 20, int c = 30) {
    cout << a + b + c << endl;
}
int Func(int a = 1, int b = 2, int c = 3) {
    return a + b;
}
int main() {
    Func(1, 2, 3);//函数类型虽不同,但当调用时两者仍都满足,所以出错
    return 0;
}


四,引用

4-1,引用的使用

       在C++中出现了与指针效果相同的引用概念,引用与指针不同的是引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间,即可通过引用来改变变量,与指针一样。

使用方法:类型& 引用变量名(对象名) = 引用实体。

void TestRef()

{    

        int a = 10;

        int& b = a;//定义引用类型

        //输出两者的地址一样,即b的改变也会影响a的改变    

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

        printf("%p\n", &b);

}

图解:

C++初阶1_第1张图片

其中引用的使用要有以下注意要点:

        1,引用类型必须和引用实体是同种类型的,指针可以不同。

        例如:int a = 5;chr &b = a;将会出错,因为变量a的类型为int,指向a的引用b的类型也必须为int。

        2,引用在定义时必须初始化。

        例如:int &c;将会出错,因为引用没有初始化

        3,一个变量可以有多个引用。

        例如:int a = 5;int &b = a;int &c = a;也就是说一个变量可有多个别名,当a、b、c其中一个改变时,这些别名和a的值都会被改变。

        4,引用一旦引用一个实体,再不能引用其他实体。

        例如:int a = 7, b = 6;int &c = a;&c = b;错误,因为引用c已经指向了一个实体,不能改变引用的指向,但是类似于c = b,c = 9等操作是赋值操作,没有改变引用的指向,是可以的。 


4-2,常引用

        常引用通常是引用常量的,也可引用变量。具体定义:const 类型& 引用变量名(对象名) = 常量。

解析说明:

        首先,我们先要明白,无论什么变量,只要加上const关键字修饰后相当于将变量的权限缩小了,即此时的变量成为了常量,不可改变,而常引用就是将引用的权限缩小,即局限了引用的作用范围,当常引用指向变量时,是将别名的权限限制了,但变量名本身还可以改变。之所以常引用可以指向变量是因为在规定中,权限小的可以指向权限大的,权限大的不可指向权限小的,而变量的值可以改变,常量的值不可改变,因此变量的权限要比常量的权限大。之所以要如此规定是因为当大权限的引用一旦指向常量时,引用改变时常量也要改变,显示是错误的,而使用const修饰后的引用本身其实就是别名,当指向大权限变量时,其实只是将别名限制,跟变量本身权限无关。指针与此也是同理。

        还有,在平常我们变量赋值操作时,如果会发生类型转换,中间都会产生一个临时变量,赋值的操作不是变量之间直接赋值,是将这个临时变量赋值,而这种临时变量具有常性,也就是此时是将常量赋值。没有类型转换将不会发生此事情,将直接赋值。如下图:

C++初阶1_第2张图片

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;// 该语句编译时会出错,a为常量,权限大的不可指向权限小的
    const int& ra = a;
    //int& b = 10;// 该语句编译时会出错,b为常量,权限大的不可指向权限小的
    const int& b = 10;

    //权限小的指向权限大的
    int c = 5;
    const int& e = c;//别名权限缩小
    cout << "c = " << e << endl;//输出5
    c = 6;
    cout << "c = " << e << endl;//输出6

    //产生临时变量的情况

    double d = 12.34;

    //int& rd = d; // 该语句编译时会出错,类型不同,系统将具有常属性的临时变量进行赋值

    const int& rd = d;//相当于const int& rd = 12,rd = 12。
}


4-3,引用的使用场景

        引用的使用场景其实跟指针的使用场景一样,唯一要注意的是引用作为函数返回值的情况。当引用作为函数的返回值时,请注意,不要返回局部变量的引用,因为局部变量是存放在内存中的栈区中,栈区的数据会随着函数栈帧的销毁而销毁,一旦栈帧销毁了局部变量的值将会随机,但是如果编译器没有及时销毁函数栈帧,这时里面的数据还存在,将不会出现错误。如下:

#include
using namespace std;
int& ADD(int a, int b) {
    int c = a + b;
    return c;
}
int main() {
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    return 0;
}

运行图:

C++初阶1_第3张图片

        可发现,本人的编译器没有及时销毁函数栈帧,即系统没有及时回收内存。但是有些编译器会及时回收内存,即销毁函数栈帧,所以,要想返回局部变量的引用,我们可用关键字static将其放入到静态区中存储,静态区中的数据只会在整个程序结束后才回收内存,不会随着函数栈帧的销毁而销毁。即如下:

#include
using namespace std;
int& ADD(int a, int b) {
    static int c = a + b;
    return c;
}
int main() {
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    return 0;
}    

分析:

        在计算机中,当函数返回值时,其实系统先将此值进行了保存,将此数据临时拷贝到了寄存器中,当返回时其实就是从寄存器中返回。所以,在以上返回局部变量的引用时笔者调用了多次,因为寄存器保留了此数据,当我们第二次调用此数据就彻底清空了,所以,只调用一次是不能确定函数栈帧是否被及时回收。

         大多数情况下,我们运用引用作为函数的返回值时是为了修改数值,在后面的学习中我们会深入运用,在这里先了解下即可。


4-4,引用的效率分析以及与指针的关系

引用的本质:

        引用的本质在C++内部实现是一个指针常量,说白了引用其实是按照指针方式来实现的,并且,在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

1,变量引用的使用

 int a = 10;

int& p = a;//系统内部自动转换为int* const p = &a;其中指针常量是指针的指向不可改,也就说明了为什么引用不可更改的原因

 p = 20;//内部发现p是引用,自动转化为*p = 20;

2,常引用的使用

 const int& p = 10;//加上const修饰后,系统内部将代码转换为int a = 10;const int& p = a;

引用跟指针和普通数值之间的效率比较:

        首先,我们来分析以值作为参数或者返回值类型的情况,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。当我们运用指针时,指针可是要在内存中开辟空间,在运算时,指针在内存中要经过复杂转换和相关逻辑的连接,这些复杂运算很容易产生错误而且可读性也差,在传值和指针作为传参以及返回值类型上效率相差就很大。引用的使用可达到指针的相同效果,虽然当函数调用时仍要给形参分配存储空间,但是引用本身就不另开空间,运算时也是跟引用指向的实体运算一样,没有复杂的逻辑结构。综上所述,引用无论在效率上还是安全性上都要比指针高。

        引用虽然相对于指针比较安全和高效,但引用本身也不是绝对安全,如果一旦使用不当也将会产生危险性。


五,内联函数

       在讲解内联函数之前我们先回顾下C语言中的宏。宏的用法跟函数类似,它比函数高效的是宏的使用不用建立函数栈帧,但宏本身也存在很多问题,我们先观察以下代码:

#define ADD(x, y) x + y;

//int a = ADD(1, 2) * 5;转换后a = 1 + 2 * 5 = 11,不符合我们的要求
#define ADD(x, y) (x + y)

//int a = ADD(1 | 2, 1 & 2); 转换后a = (1 | 2 + 1 & 2),不符合我们的要求
#define ADD(x, y) (x) + (y)

//int a = ADD(1, 2) * 5;转换后a = 1 + 2 * 5 = 11,不符合我们的要求
#define ADD(x, y) ((x) + (y));

//写法正确

        不难发现,宏的使用虽然在效率上比函数高,但本身存在很多细节观念,复杂的宏使用很容易掉坑,所以,宏有以下优缺点:

        优点: 1.增强代码的复用性。

                            2.提高性能。

        缺点: 1.不方便调试宏。(因为预编译阶段进行了替换,也就是不能调试,难以发现错误)

                    2.宏导致代码可读性差,可维护性差,容易出错。

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

        由于宏的这些原因,C++使用了以下两中方法进行改进:

                1,使用常量定义,换用const enum(即枚举)

                2,使用短小函数的内联函数

        枚举在这里我们先不讨论,我们先观察内联函数。C++中用的关键字inline修饰的函数叫做内联函数,编译时,C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,从这方面可看出,内联函数提升了程序运行的效率。

解析:

        1,inline其实是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,系统会用函数体替换函数调用,这样一来可能就会使目标文件变大,但少了调用开销,提高了程序的整体运行效率。

        2,inline虽然几乎解决了宏的所有缺陷,但是inline的使用使空间消耗太大了,所以,当函数规模较小时,编译器才会决定使用内联函数,当函数规模较大或运用了递归时,编译器将直接忽略了这种请求,即内部还是没有使用内联函数。笔者建议,对于函数规模较小且频繁调用时采用inline修饰。 

        3,不同编译器内联函数的实现机制可能有所不同,具体还要根据编译器的内部功能而定。

        4,inline函数不支持声明和定义分离开,因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数的。总的来说就是inline不支持头文件的定义。

        5,可以在同一个项目的不同源文件定义函数名相同但实现功能不同的内联函数。

举例如下:

//内部规模小,系统将展开

inline int Add(int x, int y) {
    int c = x + y;
    return c;
}

//内部规模较大,系统不会展开

inline int Add(int x, int y) {
    int c = x + y;
    int c1 = x + y;
    int c2 = x + y;
    int c3 = x + y;
    int c4 = x + y;
    int c5 = x + y;
    int c6 = x + y;
    int c7 = x + y;
    int c8 = x + y;
    int c9 = x + y * c8;
    int c10 = x + y;
    int c11 = x + y;
    return c1 + c10 - c9;
}

//运用了递归,系统将不会展开

typedef struct Node {
    int val;
    struct Node* left;
    struct Node* right;
}Node;
inline int Tree(Node* root) {
    if (!root) {
        return 0;
    }
    int leftsize = Tree(root->left);
    int rightsize = Tree(root->right);
    return leftsize + rightsize;
}

        内联函数的运用虽然具有局限性,但是它的优点不可忽视,在后期的运用也是很重要的。它可以调试,运用效率高,语法也简单,更不用建立栈帧,大大的提升了效率。


补充:本文讲解很多细节其实是为后面打基础,部分内容设计到了很多原理,这不仅是为了后面的开发打基础,还是为了后面的深入学习做铺垫,所以,一定要理解这些原理和理清这些思路,否则后面的文章学习中将会感到很困惑。最后要说的是,C++的学习本身就有很大的难度,开头其实还好,后面越深入难度越大,我们一定要有个好的心态并积极练习才能学好这门技术。

你可能感兴趣的:(1024程序员节,c++)