笔记3 · C++在编码中带来哪些改变

一、C++输入输出 cout 、cin

cout、cin均为C++内置对象。可以看作是一连串的数据流而不是关键字。C++库提供了大量的类(class),程序员可以使用他们来创建对象,cout和cin就分别是ostream和istream类的对象;这种在c++中提前创建好的对象成为内置对象。

使用前提是包含iostram头文件

cin:标准输入;

cout:标准输出

cerr:标准错误。

endl是:end of line 的缩写。

cout<< "内容" << [函数] << [表达式]  << [变量] ;等价于:printf("  %d %s %c.....", ,,,,,,,,,);

 cout << endl; 等价于 printf("\n"); 等价于 putchar('\n');

cin >> x   等价于 scanf("%d", &c);或者 scanf("%c", &c);  

cin连续输入: cin>>x>>y    等价于 scanf("%d%d", &a, &b);

 

二、C++变量能够定义在哪些位置?

一般学习C语言的时候,在书籍中可以看到;在C89中所有的局部变量都必须定义在函数开头,在定义好变量之前不能有其他的执行语句。而在C99中标准取消了这样的一个限制,但VS、VC对C99仍然保持不积极,仍然要求变量只能定义在函数开头。

在C++中就取消了这样的一个限制,变量只要在使用之前定义就好,不必强制必须在函数开头定义所有变量。

取消限制带来的另外好处就是,能够在for()循环语句中定义变量;例如:for(int i = 0; i < N; i++){....;;;}

在for()循环语句内部定义变量会让代码看起来更加紧凑,并使得变量i的作用域限制在整个for()循环语句中(包括循环条件以及循环体)。

 

三、新增布尔(bool)类型

C语言没有彻底从语法上支持“真”与“假”,只是用0 和 非零来表示  例如:条件语句if( !swap() ) 的含义就是swap()函数返回值为非零既为假,零既为真! “!”单目运算符是逻辑取反的意思 和“~”按位取反不同;他的规则是非0即1,存在则为真。

C++中增加了这种类型(bool是关键字),用bool 定义出来的变量,它一般占用一个字节的长度。取值为真(1)或假(0),也就是开关变量。

其中在编码中,满足非0即1规则。适合用在逻辑运算,关系运算和开关变量的值。

#include <iostream.h>
using namespace std;

//非零即一
main(void)
{
    bool a = true;
    int b = -5;

    a = b;
    cout << "a = " << a << endl;

    a = false;
    cout << "a = " << a << endl;
	
    b = a;
    cout << "b = " << b << endl;
	
    return 0;
}

 

四、const关键字在C++中的改变

C++中的const更像编译阶段的#define。例如在C中定义 const int a = 10;就意味a变量这个值不可被修改。而C中却存在着一个BUG能够通过指针的改变来间接修改其中的值。

C++在此做了改进。使其变量不存于内存中,而将其存于一个名为:符号表的空间里,有关符号表:C++编译器符号表有哪些内容?

我们只要知道符号表内的内容不在内存中即可(说明它不可被修改)。在符号表中的内容仍然能够取地址,来访问它,但不能修改它。

  • 有关宏定义和const初始化的区别:

1.#define没有类型,#define的操作在预处理阶段

2. 常量初始化则是在编译阶段替换,编译预处理时属于直接的“文本替换”。而const处于编译期处理的。const修饰的变量值只能初始化一次且不可被更改,但变量值仍能用取地址符取得它的地址。

3.const可以修饰更复杂的数据类型(比如后期要总结的结构体,类中成员变量,以及成员函数;和其使用时的各种注意点)。

4.#define没有词法规则,和优先级。

#include <iostream>
using namespace std;

main(void)
{
    const int ret = 10;

    int const *p = (int *)&ret;

    (*p) = 20;

    cout << "ret = " << ret << endl;
    cout << "*p = " << (*p) << endl;

    return 0;
	
}

  • 有关作用域:C++中全局const作用域是当前文件。
  • 在多文件中的改变:

在文件*1.c中 const关键字如修饰全局变量(例如:const int a = 10;),在 *2.c中使用extern关键字修饰同名变量n ,在C语言中能够正确编译。

而在C++中,规定:全局const变量的作用域仍然是当前文件,但是它在其他文件中是不可见的。这和添加了static关键字的效果类似。虽然在*2.cpp中使用extern关键字声明了变量a,但是在链接时却找不到代码段*1.cpp中的n。

由于C++全局const变量的可见范围仅限于当前源文件,所以可以将它放在头文件中,这样即使头文件被包含多次也不会出错。

关于const变量修饰的远不止这些,后面还会接触到const修饰成员数据,const修饰成员函数,const修饰成员对象等。

 

五、C++动态分配内存new运算符和释放内存delete 运算符

在C语言中我们动态分配内存,使用标准库函数中的malloc()函数;释放内存则用free()函数。如下所示:

int *p = (int *)malloc(sizeof(int) * 10);//堆区中分配十个int型的内存空间

free(p);//释放内存

在C++中,这两个函数仍然能够使用,但是C++有新增了两个关键字:new和delete:

new用来动态分配内存,delete用来释放内存。使用方式:

int *p  = new int;//申请1个int型的内存空间

delete p;//释放内存

new操作符会根据后面的数据类型来判断所需空间的大小。

如果希望分配一组连续的数据。

int *p = new int[10]; //分配10个int型的连续的空间

delete []p;//释放这些连续的空间

new[]分配的内存,需要delete[]释放,他们一一对应。

使用new创建对象:

1. student  *stu = new student;//在堆区创建对象

2. student  *stu = new student(1, "@", 3.14);//在堆区创建对象,并向构造函数传入参数

3. student *stu  = new student[2] = {4, 5}; //创建了2个数组对象, 并为其数组对象初始化了两个构造函数参数值。没有则调用默认构造函数,后期笔记补充。

4. delete stu;//释放1和2

5. delete[] stu;//释放3

我个人比较喜欢学习后把今天的知识点全诺列在一道程序中完成一个总的目标来演示,这样具有代表性,在编码时更能发现和探索问题。哪些是C++不允许的,又有哪些是极端的。这样才能融汇贯通。

有关new/delete  malloc()/free()的区别:

最大的区别:new在申请空间的时候会调用构造函数,malloc不会调用
申请失败返回:new在申请空间失败后返回的是错误码bad_alloc,malloc在申请空间失败后会返回NULL
属性上:new/delete是C++关键字需要编译器支持,maollc是库函数,需要添加头文件
参数:new在申请内存分配时不需要指定内存块大小,编译器会更具类型计算出大小,malloc需要显示的指定所需内存的大小
成功返回类型:new操作符申请内存成功时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,因此new是类型安全性操作符。malloc申请内存成功则返回void*,需要强制类型转换为我们所需的类型
自定义类型:new会先调operator new函数,申请足够的内存(底层也是malloc实现),然后调用类的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数来释放内存(底层是通过free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构函数
重载:C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回地址。malloc不允许重载。

为什么有了malloc()、free()还要有new、delete?

上面说了,new分配空间的时候可以大大减轻程序员的编码工作;就比如说:对一个对象指针分配空间:stu *pobj = new stu;

即可,他不要类型严格的匹配,更不会显示的计算出所需内存的大小,new会根据你的类型,自动推倒出所需多大空间;此外,malloc()的返回值是void *,"万用等于无用" ,这些工作还需要程序员显示的将返回值处强制类型转换成自己想要的类型:

For example: char *str = (char *)malloc(sizeof(char) * 10);

 

六、C++内联函数inline详解

1. 什么是内联函数:

在C语言编码中,我们知道一个C程序执行的过程 (就是各个函数调用的过程) 可以是由多个函数之间相互调用的过程,他们形成了一条或多条复杂的调用链,可以是普通调用,也可以是回调函数,函数指针数组等等。这个链条的起点是main() 终点仍然是main(), mian()调用完了,它会返回一个值给系统,来结束自己的生命,从而结束整个进程。

函数的调用是由时间和空间开销的。如果函数代码体代码较多,那么就需要较长的时间,在函数调用机制占用的时间就可以忽略, 但是函数体代码较短时,就一两条语句,那么大部分时间都会花费在函数调用机制上。这种时间开销就不可忽视。

为了消除这种时间和空间的开销,C++提供了一种高效的方法。即:在编译时将函数调用处的函数体替换,这有点类似于C语言中的宏展开。这种在函数调用处直接镶嵌函数体的函数称为---内联函数。

指定内联函数的方法也很简单:

只需要在函数定义处添加inline关键字即可。

#include <iostream>
using namespace std;

inline void swap(int *a, int *b);//函数声明, 这里我在声明处也加了inline关键字 
//目的是为了告知自己inline  是一个实现关键字而不是一个声明关键字
//虽然编译能够通过,但是这种做法是无效的

inline void swap(int *a, int *b)//函数定义处加上inline关键字
{
    int tmp = *a;
    *a = *b;
    *b =tmp;
}


int main()
{    
    int a = 10, b = 20;
    
    swap(&a, &b);

    cout<< "a = "<< a << " "<< endl;
    cout << "b = "<< b << " " << endl;
    
    return 0;
}

注意:要在函数定义处添加inline关键字, 在函数声明处添加inline关键字编译器不会报错,但是这种做法是无效的,编译器会忽略在声明处的inline关键字。-----inline是一种用于实现的关键字。

使用内联函数的缺点:编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常的大,那么编译后的程序体积也将会变得很大,所以一般只讲那些短小的,频繁调用的函数声明为--内联函数。

2、内联函数与宏定义

宏定义是一项细思极密的工作,在编码时很容易踩坑,一般都会出现意想不到的结果。所以在编写宏函数时一般要尽可能多的使用括号

而在C++中创造了内联函数,所以我们一般会用内联函数来代替宏,情况就没那么复杂了。

和宏一样,内联函数可以定义在头文件中(不用加static关键字),并且头文件被引用多次include后也不会引起重复定义的错误。

综上所述,可以看到内联函数主要有两个作用,一是消除函数调用时的开销,二是取代带参数的宏。

关于宏的缺点如上const总结,使用内联函数取代带参数的宏更能凸显内联函数存在的意义。

3.  内联函数的多文件编译

将内联函数声明和定义分散到不同的文件中会出错

//main.cpp
#include <iostream>
using namespace std;

void func();

main()
{
    func();
    return 0;
}
//multi.cpp
#include <iostream>
using namespace std;

inline void func()
{
    cout<<"inline func() was called"<<endl;
}

这两个文件编译时能够正常编译,但在链接时会报错。因为func()时内联函数,编译器件会用它来替换函数调用出,编译完成后函数就不存在了,所以链接器在将多个目标文件合并和一个文件时找不到func()的定义,所以会产生链接错误。

总结:内联函数虽然叫作函数,在声明和定义的语法上也和普通函数一样,但他已经失去了函数的本质。函数一段可以重读利用的代码,它位于虚拟地址空间中的代码区中,也占用文件的体积,而内联函数的代码在编译后就被消除了,不存在于虚拟地址的空间中,无法重复使用。

内联函数和宏:

首先内联函数是函数,处于编译阶段与调用处的替换,替换完毕后这个远处的代码就没了。

宏是文本替换,不是函数,没有更多的词语法规则及优先级。且宏处于预处理阶段的文本替换。

内联函数可以GDB调试,而宏不可以调试

 

七、C++默认参数

什么是默认参数?---当函数调用时省略了一个实参,并自动使用一个值,这个值就是给形参指定的默认值。

定义函数时,我们可以给形参一个默认的值,这样调用函数时,如果没有给这个形参赋值(没有对应的实参), 那么就使用这个默认的值。也就是说,调用函数可以省略有默认值的参数。

如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值

C++规定,默认参数只能放在形参列表的最后而且一旦为某个形参指定了默认值那么它后面的所有形参都必须有默认值。实参和形参的传值。实参和形参的传值是从左往右依次匹配的默认参数的连续性是保证正确传参的前提

//以下写法是正确的

void func(int a, int b = 10,  int c =20);

void func(int a, int b, int c = 20);

//以下写法是错误的

void func(int a, int b = 10, int c);

void func(int a, int b = 10, int c, int d = 30);

默认参数并非编程方面的重大突破,而是提供了一种便捷的方式。在以后设计类的时候,通常使用默认参数来为构造函数和析构函数服务, 可以减少要定义的析构函数、方法及方法重载的数量。

到底是在声明中指定默认参数,还是在定义中指定默认参数

C++规定, 在给定的作用域中只能指定一次默认参数

对于使用多文件编程的话,则不受此规则影响 ;如:在aaa.h中声明一个带有默认参数的函数, 在aaa.c中实现时仍然指定也无关系。另外,形参名在两个文件中都可以使用不同名称。

 

八、C++函数重载

函数重载,指的是在同一个作用范围內(同一个类、同一个命名空间等)使用同一个函数名,能够实现几个功能类似的方法,只是有些细节方面不同 比如说参数不同的函数。

函数重载的规则:

1. 函数名称必须相同。

2. 参数列表可以不同。

3. 函数的返回类型可以相同也可以不同。

4. 仅仅返回类型不同不能作为函数重载的依据。

在使用重载函数时,同名函数功能应相同或相近, 不要用同一函数名去实现一个完全不相干的功能,虽然合法,但尽量别去那么做。

默认参数,内联函数, 重载, 有时也能一起使用。如下例子所示:

#include <iostream>
using namespace std;

int Min(int a, int b = 3);
int Min(int b, int a, int c);//重载,这里不能给第三个参数指定默认参数,否则同样产生二义性
//int Min(int a, float b = 3.14);//产生二义性,因为上面指定了默认参数
float Min(float a, float b = 3.14);//返回值不同不能作为重载依据
char Min(char a, char b = 'z');//他们是函数名相同

int main()
{
    int a = 10, b = 2;
    cout<<"int Min() = "<<Min(a)<<endl;//3
    cout<<"int Min() = "<<Min(a,b)<<endl;//2
    cout<<"int Min() = "<<Min(b,a,1)<<endl;//2
    cout<<endl;

    float c = 4.31, d = 1.34;
    cout<<"float Min() = "<<Min(c)<<endl;//3.14
    cout<<"float Min() = "<<Min(c,d)<<endl;//1.34
    cout<<endl;
	
    char e = '0', f = 'f';
    cout<<"char Min() = "<<Min(e)<<endl;//0
    cout<<"char Min() = "<<Min(e,f)<<endl;//0

    cout<<endl;
	
    return 0;
}


inline int Min(int b, int a, int c)
{
    if(a < b)
    {
	if (a < c)
	    return a;
	else
	    return c;
    }
    else{
	if(b < c)
	    return b;
	else
	    return c;
    }
}

inline int Min(int a, int b)//同一文件中默认参数只能被指定一次
{
    return ((a < b) ? a : b);
}


float Min(float a, float b)
{
    return ((a < b) ? a : b);
}

char Min(char a, char b)
{
    return ((a < b) ? a : b);//ASCII compare
}

 

九、C++引用符 --- “&”

C++除了函数重载,也有运算符重载(在后面会一步一步的学习到),就比如说C++引用:“ & ”, 我们也知道它同样也是一个取地址符。一个运算符拥有两种作用,那么就称这个运算符被重载了。

我们知道,参数传递的本质上是一次赋值的过程,赋值就是对内存的一次拷贝。所谓内存拷贝就是指,一块内存上的数据拷贝到另一块内存上。我们也知道串值的话,不会用到到原来内存空间的数据,如果是传地址的话,则会影响到原来内存块里的数据。

C/C++禁止在函数调用时直接传递数组的内容,而是强制转换数组指针。

在C++中,有了种比指针更加便捷的传递聚合类型数据的方式-----引用:“ & ”。

#include <stdio.h>

void swap1(int a, int b)//传值
{
    int tmp;
    tmp = a;
    a = b;
    b =tmp;

}

void swap2(int *a, int *b)//传址
{
    int tmp = *a;
    *a = *b;
    *b =tmp;
}

int main(void)
{
    int a =10, b =20;
    swap1(a, b);
    swap2(&a, &b);


    return 0;
}
#include <iostream>
using namespace std;

void swap(int &a, int &b)//采用C++应用
{
    int tmp = a;
    a = b;
    b =tmp;
}


int main(void)
{
    int a = 10, b = 20;
    
    swap(a, b);

    cout<<a = << a << " " << "b = "<< b << endl; 
    return 0;
}

这两个函数的功能就是实现两个数据的交换,到底哪个才是真正的原内存块交换呢?

答案是: C址传递, C++引用传递

引用是C++相对于C的又一个扩充。引用可以看作是数据的一个别名, 通过这个别名和原来的名字都能找到这份数据。引用就类似与windows中的快捷方式, 一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序。引用还类似与人的绰号、笔名。

引用的定义方式类似与指针的定义方式,只是用“ & ”取代了“ * ” ,

type  &name = data;

引用必须要在定义的同时初始化,并且以后也要从一而终,不能再应用其他数据,这有点类似于常量(const)

c++应用可以作为函数参数、可以作为函数的返回值

上面的代码就是引用作为函数参数来运行的。在定义或声明函数的时候,我们可以将函数的形参指定为引用的形式,这样在函数调用时就会将实参和形参绑定到一起,他们都指代了同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有"在函数内部影响函数外部数据"的效果。

引用是基于指针实现的。但它比指针更加易用。

当引用作为返回值的注意点:

#include <iostream>
using namespace std;

int &add(int &val)
{
    static int sum = 0;
    for(int i = 0; i <= val; i++)
	sum += i;

    return (int &)sum;//不能将局部变量作为引用返回值
}

main(void)
{
    int a /*, b*/;
    cout << "please input a value" << endl;
    cin >> a;
    int &result = add(a);
    /*int result  = add(a);上面两种方式都是正确的*/
	
    cout << "result = " << result << endl;
	
    return 0;
}

..................................................................................................................

C++引用作为函数返回值时,需要注意一个小问题:就是不能将局部变量作为函数的返回值,因为当函数调用完毕后局部数据会被出栈销毁,有可能下次使用时就不存在了,C++不允许有这样的操作存在, 检测时会出现对应的警告。

C++在本质上和指针有什么区别?

引用虽然时基于指针实现的,但它比指针更加易用;通过指针获取数据时要加“ * ”, 书写麻烦, 而引用不需要,他和普通变量的使用方式一样。

1. 引用必须要在定义式初始化,并且以后还要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据

2.  可以有const 指针, 但是没有const引用。 不能定义成: int a = 20; int &const r = a;  但可以有:const int &a 它被用来绑定到临时数据。

3. 指针可以有多级而引用只有一级, 例如:int **p;是合法的 而 int &&p是不合法的。 如果要用另一个引用变量来指代一个引用变量,加一个&即可。即:int a = 10; int &r = a; int &rr = r.

4.  指针和引用的自增和自减运算意义不一样。对指针自增(*p++) / *(p++)指向的是下一份数据,而对引用的自增等价于(*p)++; 自减也是一样的道理。

C++引用不能直接绑定到临时数据!

引用和指针在本质上是一致的,引用仅仅是对指针进行了简单的封装引用和指针都无法绑定到无法寻址的临时数据,例如:

int *p = &(100);//常量表达式值虽存在内存中,但无法寻址,所以也不能用&符来获取到它的地址,更不能用指针指向它。

int *p = &(23 + 34 * 2);

int n = 100, m = 200;

int *p1 = &(m + n);

int *p2 = &(n + 100);

bool *p4 = &(m < n);

int *p = &( func() );

以上定义尝试用&来获取他们的地址都是错误的

如何解决?-----使用常引用在引用的最前面使用const修饰符修饰

1. 我们知道,当引用绑定到一份数据后,就可以通过引用来对这份数据进行操作了,包括读取和写入;尤其是写入操作,会改变其数据的值而临时变量数据往往无法直接寻址, 是不能写入的, 即使为临时数据创建了一个临时的变量,那么修改的也仅仅是临时变量里面的数据,不会影响到原来的数据这样就使得引用所绑定的到的数据和原来的数据不能同步更新了所以最终产生了两份不同的数据,失去的引用的意义

2. const引用和普通引用不一样, 我们只能通过const引用读取数据的值, 而不能修改其中值, 所以不用考虑同步更行的问题, 也不会产生两份不同的数据,为const引用创建的临时变量,反而会使得引用更加灵活通用。

总之 ,这就和const 修饰指针一样,只不过const修饰指针时可以这么修饰,分别表示:

const int *a;//指针指向的值不能修改 ,就跟常引用一样

int const *a;//指针的本身不能被修改

只不过C++引用中只允许定义成:const  int&  a = b;  或他现在就能这么定义 int m  = 100;  int n = 200;  const int& r = (m+n);

const引用类型允许绑定到类型不一致的数据上:

int n  = 100;

int &r1 = n;//合法

const float &r2 = n;//合法

char c = '@';

char &r3 = c;

const int &r4 = c;//正确

引用类型的函数形参应尽可能地使用const。如果期望在函数体内不会修改到引用绑定的到的数据。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值