C++之C语法增强_寄存器_三目_引用初探


站在编译器和C的角度剖析c++原理, 用代码说话


Hello world

首先我们先引入无敌案例之hello world:

#include <iostream>
using namespace std;
int main(void) {
    std::cout << "Hello, World!\n";
    int i = 0;
    cin >> i;
    cout<<"你输入的i是"<<i<<endl;
    return 0;
}

cout就是表示标准输出终端, 但是这里用了<<来表示,我们知道在C中这表示的是移位运算符,但是这里却表示输出重定向?这就说明c++编译器对这个符号做了增强的功能. 这个话题我们在之后的文章中会讲到,这也是个重点.

面向对象VS面向过程

我们都知道c是面向过程的语言,c++是面向对象的语言, 那么到什么是面向过程什么是面向对象呢,对于初学者来说的确很抽象,看到网上的一些概念更加混乱了,那么我们用代码说话:

//面向过程
int main(void){
    double r = 0;
    double s = 0;
    cout << "请你输入圆的半径";
    cin >> r;
    s = 3.14 * r * r;
    cout << "圆的面积是:" << s << endl;
    return 0;
}
//面向对象
struct Circle{
    double m_r;
    double m_s;
    void setR(double r){
        m_r = r;
    }
    double getS(){
        m_s = 3.14*m_r*m_r;
        return m_s;
    }
};
int main(void){

    Circle c1;
    double r = 0;
    cout << "请你输入圆的半径";
    cin >> r;
    c1.setR(r);
    cout << "圆的面积是" << c1.getS() << endl;
    return 0;
}

面向过程的代码我就不解释了, 面向对象的这个代码中首先说的是定义Circle这个结构体数据类型,我们知道数据类型的本质是什么?就是固定大小的内存块的名字而已. 所以我们当然可以在c++中继续使用struct, 但是在c++中struct相比于C的变化是能够在结构体中定义方法,能够在里面继承函数, 这也就是封装的概念. 当然在后面更多的是使用class而不是struct.我们先循序渐进的给大家认识本质,让大家看到c++其实不恐怖. c++中的struct和class还有一点不同是struct中定义的方法默认属性是public, class中默认是private的,别急,这些后面都会涉及到,这里只需要知道class很类似struct就行.
struct类型的加强:
C语言的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型.
C++中的struct是一个新类型的定义声明,进行了功能增强,内部还能加函数.
在c++的struct或class中的这些变量m_r叫做成员变量,void setR(double r){}这些方法叫做成员方法.
在main中,Circle c1;就是用类型定义变量,就是分配了个内存的意思. 只有执行c1.setR(r);c++编译器才会去调用函数.
我们这里介绍个易犯错误模型:

class circle{
public:
    double r;
    double pi = 3.14;
    double area = pi * r * r;
};
int main01_04(void){
    circle p1;
    cout << "请输入area" << endl;
    cin >> p1.r;
    cout << p1.area << endl;
    return 0;
}

这样的结果是个乱码,为什么呢?是不是你也错了?这个错误知识点上面已经提到过了, 再分析一遍: circle p1;分配了内存,好,r因为没有初始化,所以r的内存中是乱码, pi是3.14, 那么现在area的内存中就是乱码. 接着将终端输入给r的内存赋值,好,这时候r的不是乱码了,但是对area并无关系,所以还是乱码. 怎么处理呢?当然是像上面一样在里面写方法来调用.

namespace

这里先说一下每次写c++都要打头写#include <iostream>, 我们知道这是为了包含头文件,但是为啥不写#include iostream.h呢?这是因为当时设计者为了和C有些大点的区别而做的一些并无卵用的活. 所以没多大意义. 并且当我们使用#include <iostream>的时候如果没有写下面的using namespace std;就不能使用好多定义比如cout,你要想使用就得必须写成std::cout这种形式,那这到底是为什么呢?那么我我们看看iostream里面到底是什么鬼?

#ifndef _LIBCPP_IOSTREAM
#define _LIBCPP_IOSTREAM
/*
    iostream synopsis
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

namespace std {

extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;

}  // std

*/

#include <__config>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

_LIBCPP_BEGIN_NAMESPACE_STD

#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;

_LIBCPP_END_NAMESPACE_STD

#endif  // _LIBCPP_IOSTREAM

我们会发现它定义了好多cout, cin等等,但是它本身也没有加using namespace std;这个语句,也就是说要想它本身定义的cout等用起来,就得配合using namespace std;. 好,那么我们就说说这个namespace到底是什么鬼?
namespace就是标准作用域, c++标准库的所有定义都被扔在了一个叫std的namespace中. 在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。std是c++标准命名空间,c++标准程序库中的所有标识符都被定义在std中,比如标准库中的类iostream、vector等都定义在该命名空间中,使用时要加上using声明(using namespace std) 或using指示(如std::string、std::vector<int>).C++命名空间的定义:namespace name { … }.
C++命名空间的使用:
使用整个命名空间:using namespace name;
使用命名空间中的变量:using name::variable;
使用默认命名空间中的变量:::variable;
默认情况下可以直接使用默 认命名空间中的所有标识符

namespace NameSpaceA
{
    int a = 0;
}
namespace NameSpaceB
{
    int a = 1;
    namespace NameSpaceC
    {
        struct Teacher
        {
            char name[10];
            int age;
        };   
    }
}
int main()
{
    using namespace NameSpaceA;
    using NameSpaceB::NameSpaceC::Teacher;  //::域作用符
    printf("a = %d\n", a);
    printf("a = %d\n", NameSpaceB::a);
    Teacher t1 = {"aaa", 3};
    printf("t1.name = %s\n", t1.name);
    printf("t1.age = %d\n", t1.age);
    system("pause");
    return 0;
}

c++之register关键字

register:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。注意是尽可能,不是绝对。你想想,一个CPU 的寄存器也就那么几个或几十个,你要是定义了很多很多register 变量,它累死也可能不能全部把这些变量放入寄存器吧,轮也可能轮不到你.

int main()
{
    register int a = 0;
    printf("&a = %x\n", &a);
    system("pause");
    return 0;
}

早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充.
c++中的编译器对register进行了功能增强, 即使没有使用register修饰变量,c++也会自动优化常用的变量.

int main(void){
    register int i = 0;
    int b = 0;
    for (i = 0; i < 10000; i++) {
        ;
    }
    printf("%d \n", &i);
    return 0;
}

register关键字 请求编译器让变量i直接放在寄存器里面,速度快, 在c语言中 register修饰的变量 不能取地址,但是在c++里面做了内容. register关键字请求”编译器”将局部变量存储于寄存器中, C语言中无法取得register变量地址, 在C++中依然支持register关键字, C++编译器有自己的优化方式,不使用register也可能做优化,会将经常使用的变量自动放在register中. C++中可以取得register变量的地址, 但是C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效。
但是使用register修饰符有几点限制:
首先,register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数.
其次,因为register变量可能不存放在内存中,所以不能用”&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。
早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。

c++之boolean运算

C++在C语言的基本类型系统之上增加了bool, C++中的bool可取的值只有true和false, 理论上bool只占用一个字节,如果多个bool变量定义在一起,可能会各占一个bit,这取决于编译器的实现. true代表真值,编译器内部用1来表示, false代表非真值,编译器内部用0来表示. bool类型只有true(非0)和false(0)两个值. C++编译器会在赋值时将非0值转换为true,0值转换为false.

int main(void){
    bool b1;
    b1 = 4;
    printf("b1 = %d\n", b1);//1
    b1 = -1;
    printf("b1 = %d\n", b1);//1
    b1 = 0;
    printf("b1 = %d\n", b1);
    int a;
    bool b = true;
    printf("b = %d, sizeof(b) = %d\n", b, sizeof(b));//1, 1
    b = 4;
    a = b;
    printf("a = %d, b = %d\n", a, b);//1, 1
    b = -4;
    a = b;
    printf("a = %d, b = %d\n", a, b);//1, 1
    a = 10;
    b = a;
    printf("a = %d, b = %d\n", a, b);//10, 1
    b = 0;
    printf("b = %d\n", b);//0
    return 0;
}

c++之三目运算

首先我们考虑C中的三目运算符:

int main()
{
    int a = 10;
    int b = 20;
    (a < b ? a : b )= 30;
    printf("a = %d, b = %d\n", a, b);
    system("pause");
    return 0;
}

这个在C编译器是报错的,三目运算符是一个表达式 ,表达式不可能做左值. (a < b ? a : b )是一个表达式, 表达式的值在寄存器中,寄存器中是没有地址的,给一个没有地址的存储区赋值肯定gg. 但是在c++中可以, 说明c++编译器进行了功能增强, 返回的是a本身. 元素当左值的必要条件就是,元素有地址空间.那么我们就能这样的改这个代码在C中也能实现左值功能:
*(a < b ? &a : &b) = 30;, 所以说C++到底干了个啥,其实就是干了这个.
比如说(a < b ? 100 : b)=30这就错了,100的地址是什么鬼?所以说三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用,说白了就是没地址, 回归之前提到的元素当左值的必要条件就是,元素有地址空间.
总结一下:
C语言中的三目运算符返回的是变量值,不能作为左值使用, C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方,抛砖引出了我们下面讨论以及下一篇文章主要讨论的重点—引用.

c++之const

const在C中其实是个”冒牌货”,让我们开一段C:

int main()
{
    const int a = 10;
    int *p = (int*)&a; 
    printf("a===>%d\n", a);
    *p = 11;
    printf("*p===>%d\n", *p);//11
    printf("a===>%d\n", a);//11
    return 0;
}

在C中虽然用const定义了a, 也就是说我们不能直接去修改a的值,a变成了只读的, 但是我们通过指针p指向a的地址,然后间接的去修改a的地址指向的内存空间, 那么a的值也就变了.是不是感觉被骗了的不爽. 但是在c++中上面的这段代码就是a返回的依然是10. 这是为什么呢?首先能分析出来的是*p所指的内存空间和&a不一样. 这里解释一下c++编译器对const做了什么手脚:
C语言中的const变量, C语言中const变量是只读变量,有自己的存储空间. 然后在c++中, 一旦用const神明内部就维护了一个符号表, a->10. 这个也是只读的. 编译过程中若发现使用常量则直接以符号表中的值替换, 编译过程中若发现对const使用了extern或者&操作符,则给对应的常量分配存储空间(兼容C), 也就是说C++中的const常量, 并不会给分配空间而是仅仅保存在了符号表中, 只有在&或extern的时候,才会给分配个空间供你随便玩,但是这个空间是不会对之前符号表中的值造成任何影响的. c++中的const修饰的,是一个真正的常量,编译期间就定下来了, 而不是C中变量(只读). 那就有一个问题,这岂不是和宏定义一样了,的确是这样,它们很想,但是也是有不同的:
c++中的const常量类似于宏定义const int c = 5; ≈ #define c 5, C++中的const常量在与宏定义不同, const常量是由编译器处理的,提供类型检查和作用域检查, 宏定义由预处理器处理,单纯的文本替换.

void fun1()
{
    #define a 10//从这行往下所有的代码都能使用a
    const int b = 20;//只有在作用域内能使用
    //#undef//但是也是可以卸载掉
}
void fun2()
{
    printf("a = %d\n", a);
    //printf("b = %d\n", b);
}

const int a = 1;int const b = 1;是一样的.

char buf[20];
getMem(getMem(&buf));

这种情况下C语言是通过的,但是在c++中是不通过的. 本身这个语句就是很危险的, 因为这里其实char **char (*)[20]是不一样的. char (*)[20]是指向数组的指针, 你一旦写buf = buf + 1; 然后getMem(&buf)就gg. 所以千万不要将buf地址直接放进去了,一般都是定义一个指针char *p = NULL. 然后再传入p的地址在函数中分配.
还有是区分一些:const Teacher *p, Teacher *const p:
Teacher *const p:表示常指针,表示这个指针只能指向一个地方,也就是p不能再赋值其他的地址.
const Teacher *p:表示指向常量的指针, 也就是说这个指针指向的常量不能再赋值其他. *p不能再进行赋值.

c++之引用初探

变量名实质上是一段连续存储空间的别名,是一个标号(门牌号), 程序中通过变量来申请并命名内存空间, 通过变量的名字可以使用存储空间. 那么对一段连续的内存空间只能取一个别名吗?在c++中当然不是的.
引用是C++的概念,属于C++编译器对C的扩展.引用基于C的本质就是它是个常指针.

nt main()
{
    int a = 0;
    int &b = a; //== int * const b = &a 
     b = 11;  //== *b = 11;
    return 0;
}

请不要用C的语法考虑, 引用可以看作一个已定义变量的别名, 引用作为函数参数声明时不进行初始化.

nt main()
{
    int a = 10;
    int &b = a;
    //b是a的别名,请问c++编译器后面做了什么工作?
    b = 11;
    cout<<"b--->"<<a<<endl;
    printf("a:%d\n", a);
    printf("b:%d\n", b);
    printf("&a:%d\n", &a);
    printf("&b:%d\n", &b);//请思考:对同一内存空间可以取好几个名字吗?
    system("pause");
    return 0;
}

普通引用在声明时必须用其它的变量进行初始化, 为什么要初始化呢,就是因为上面提到的引用的本质就是常指针,const定义的能不进行初始化吗?
引用作为其它变量的别名而存在,因此在一些场合可以代替指针, 引用相对于指针来说具有更好的可读性和实用性.

int swap1(int &a, int &b){
    int tmp = a;
    a = b;
    b = tmp;
    return 0;
}
int swap2(int *a, int *b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
    return 0;
}

那么普通的引用有自己独立的空间吗?当然是有的,让我们敲一下这段代码:

struct Teacher {
    int &a;
    int &b;
};
int main()
{
    printf("sizeof(Teacher) %d\n", sizeof(Teacher));//8
    return 0;
}

有人在这里会问为什么是8,为什么不需要初始化?不是说需要初始化吗?首先这是常指针是个指针,所以8就知道了吧,初始化的问题,结构体的定义,只能告诉编译器,里面有什么类型的数据,占有多少字节,是不能在里面赋值的。构造函数是类定义里的,在那里面是可赋初值的. 也就是说参数列表,类成员中是可以不初始化话的,在调用函数, 构造对象时才算初始化.
从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏.
C中间接赋值的三个条件:
1. 定义两个变量 (一个实参一个形参)
2. 建立关联 实参取地址传给形参
3. *p形参去间接的修改实参的值.
那么引用就是将后两步c++编译器帮我们手动取了一个实参地址, 传给了形参引用(常指针).

struct Teacher{
    char name[64];
    int age;
};
//以前我们这样写
void printTe(Teacher *p){
    printf("%d\n", p->age);
}
//现在我们用引用了
void printTe(Teacher &t2){//t2就是t1的别名,不存在深浅拷贝问题
    printf("%d\n", t2.age);
}
int main(void){
    int a = 10;//c编译器分配四个空间的内存,能不能在这块内存给a再取一个别名
    //引用的语法:Type& name = var;
    int &b = a;//表示b就是a的别名
    a = 11;//直接赋值
    {
        int  *p = &a;
        *p = 12;//C中的间接赋值
    }
    b = 14;//这样b和a都是14了

    Teacher t1;
    Teacher &t2 = t1;
    t1.age = 10;
    printTe(&t1);
    printTe(t1);
}

容我给你分析一下引用的过程: 当Teacher &t2 = t1;时,进行的内部操作就是Teacher * const t1 = &t1., 然后执行printTe(t1);的时候, 调到了函数参数列表那里,void printTe(Teacher &t2){C++编译器发现呀?是个引用类型,那我给你转一下: Teacher * const t2. 所以printTe(t1);这一步就等价于printTe(&t1)过来了. 然后在函数内部编译器又自动给我们取地址间接改变了值. (注意常指针是可以取改变指针指向的内存空间的值的).
引用一旦生出来,就必须给它赋个值, 就类似于buffer, char buf[20];一出现就是个常指针(注意与常量指针是不同的).初值就是内存首地址.


联系方式: reyren179@gmail.com

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值