C++学习之路-由浅入深(快速掌握其基础)

版权声明:转载请注明出处,谢谢配合 https://blog.csdn.net/qq_33750826/article/details/79950327

一、声明

因为此前作者学过oc,c,java……..,因此学习c++就不会细聊,讲解多数在用c语言和C++作对比,此文章不适合初学编程的人,只适合已经有编程基础的人,谢谢!

二、输入输出函数

C++   是cin >> i;输入;  cout << i <<endl; 输出
C     是scanf("%d",&i); 输入  printf("i=%d",i)输出;

好处:不用像C语言一样写入对应数据类型的占位符,方便快捷。

三、Class

C++ 类:

//通过class关键字概括
class man
{
 public:
    char name[100];
    int age;
    int sex;
  private:

};

C++引入类,而C没有,C的struct和C++的类很像,但是C的struct中的变量默认是公有的,而c++不是,C++结构体可以和类一样。

c语言结构体:

struct Student
{
    int age; //4
    float score; // 4/
    long id;  //4
    char sex ; //2   vc 6.0 14
};

C++结构体:

struct Student{
private:
    int age;
    char sex;
};

四、换行符

通常换行符C和C++都是用在打印输出输入的时候,那么C和C++的换行符分别是:
C:

//通过 \n 换行。
printf("wo shi c yuYan huanHangFu \n");

C++:

//通过 endl 换行
cout << 5 <<endl;

五、const关键字

C++和C一样 const 表示不可改变,常量,如下:

 const int a=0;
 //错误,不能给常量复制。
 //a=5;

六、指针转化

C++ 类型不同的指针进行赋值,必须强转 C 不用强转

int ab=1;
int * abc=&ab;
//C++错误,C语言正确
//double *adbcd=(abc;
//C++正确
double *adbcd=(double *)abc;

七、内存分配

C:

//通过malloc分配内存,free释放内存
int *p =(int *)malloc(sizeof(int));
*p=10;
free(p);

C++

/**
new分配内存,delete释放内存
new和delete是C++内建的操作符,
不需要有任何头文化,用new分配的内存必须用delete释放
*/
int *p=new int[10];
for(int i=0;i<10;i++){
    p[i]=i;
}

for(int i=0;i<10;i++){
    cout << p[i] <<endl;
}
delete []p;

七、引用

C++引入新的概念:引用
通常C语言要想交换两个数字,必须通过传入地址的方式,而C++则引入引用的概念

7.1、定义

int a=5;
int b=10;

//同一个东西
int &c=a;
printf("c=%p\na=%p\n",&c,&a);


/**
错误
int &c;
c =a
*/

//正确
int &c=a;

总结:引用必须在定义时就赋值,并且赋值的变量和该引用是同一个地址,也就是同一个在内存中指向同一个区域。

7.2、使用场景
交换两个函数的值:
C++:

void mySwap(int &a,int &b){
    int temp=a;
    a=b;
    b=temp;
}

int a=5;
int b=10;
mySwap(a,b);
cout <<"a="<< a <<endl;
cout << "b="<<b <<endl;

C:

void mySwap(int *a,int *b){
    int temp=*a;
    *a=*b;
    *b=temp;
}

int a=5;
int b=10;
mySwap(&a,&b);
printf("a=%d,b=%d \n",a,b);

看到这里有人会觉得没什么区别,那么你就错了。

1、
int &a

引用就是一个变量的别名,而不是地址,不能改变外部变量的值
引用作为函数的参数,没有出栈操作,所以操作效率高

int * a

直接拿到变量的地址,可以将其外部传进来的参数直接改变。

2.注意:

如果要使引用参数的值在函数内部不被修改,那么将引用定义为常量引用 const &

八、内联函数

参考:
https://baike.baidu.com/item/%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0/9567625?fr=aladdin

8.1、介绍

使用关键字 inline 修饰,内联函数一般只有1~5行代码,

8.2、作用

内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名,内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。

8.3、使用

内联函数必须是和函数体声明在一起,才有效。像这样的申明Inline Tablefunction(int I)是没有效果的,编译器只是把函数作为普通的函数声明,我们必须定义函数体。

inline tablefunction(int i) {return i*i};

这样我们才算定义了一个内联函数。我们可以把它作为一般的函数一样调用。但是执行速度却比一般函数的执行速度要快。我们也可以将定义在类的外部的函数定义为内联函数,比如:

class tableClass{
     private:
        int i,j;
     public:
        int add(){return i+j;}
        inline int dec(){return i-j;}
}
        inline int tableclass::GetNum({return I;}

上面申明的三个函数都是内联函数。在C++中,在类的内部定义了函数体的函数,被默认为是内联函数。而不管你是否有inline关键字。

8.4、场景

内联函数在C++类中,应用最广的,应该是用来定义存取函数。我们定义的类中一般会把数据成员定义成私有的或者保护的,这样,外界就不能直接读写我们类成员的数据了。对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们把这些读写成员函数定义成内联函数的话,将会获得比较好的效率。

class sample{
    private:
       int nTest;
    public:
       int readtest() { return nTest; }
       void settest(int i) { nTest=i; }
}

8.5、对比
内联函数的功能和预处理宏的功能相似。相信大家都用过预处理宏,我们会经常定义一些宏,如

#define TABLE_COMP(x) ((x)>0?(x):0)

为什么定义宏呢?其实跟内联函数的作用大同小异,在调用时不是发生控制转移,而是将定义的宏展开。
但是宏有如下缺点:
1、宏不能访问对象的私有成员。
2、很容易让人产生二义性。如:

#define TABLE_MULTI(x) ((x)*(x))
TABLE_MULTI(a++)
//原本我们的意愿是想 (a+1)*(a+1),但是实际:
(a++)*(a++),加入a4,结果是16a=6.

内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

九、缺省参数

9.1、概览

所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数。

9.2、使用
C++允许函数在定义的时候,提供缺省参数
如果调用函数的时候没有提供形参,那么形参的值就是缺省值

func();
*/

/**
错误
void func1(int a=10,int b){

}

正确
void func1(int a,int b=20){

}

void func(int a=10,int b=20){
cout<< a << endl;
}
*/

9.3、规则
调用时你只能从最后一个参数开始进行省略,换句话说,如果你要省略一个参数,你必须省略它后面所有的参数,即:带缺省值的参数必须放在参数表的最后面。

十、函数模板

10.1、概览

函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。

10.2、简介

函数模板定义的一般形式如下:
template<类型形式参数表>
返回类型 函数名(形式参数表)
{
... //函数体
}

使用 template 关键字修饰。
10.3、使用场景

double max(double first, double second)
{
return first>second? first : second;
}
complex max(complex first, complex second)
{
return first>second? first : second;
}
date max(date first, date second)
{
return first>second? first : second;
}

这样不但重复劳动,容易出错,而且还带来很大的维护和调试工作量。更糟的是,即使你在程序中不使用某个版本,其代码仍然增加可执行文件的大小,大多数编译器将不会从可执行文件中删除未引用的函数。
用普通函数来实现抽象操作会迫使你定义多个函数实例,从而招致不小的维护工作和调试开销。解决办法是使用函数模板代替普通函数。

// file max.h
#ifndef MAX_INCLUDED
#define MAX_INCLUDED
template <class T>
T max(T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}
#endif

定义 T 作为模板参数,或者是占位符,当实例化 max()时,它将替代具体的数据类型。max 是函数名,t1和t2是其参数,返回值的类型为 T。你可以像使用普通的函数那样使用这个 max()。编译器按照所使用的数据类型自动产生相应的模板特化,或者说是实例:

int n=10,m=16;
int highest = max(n,m); // 产生 int 版本
std::complex c1, c2;
//.. 给 c1,c2 赋值
std::complex higher=max(c1,c2); // complex 版本

上述的 max() 的实现还有些土气——参数t1和t2是用值来传递的。对于像 int,float 这样的内建数据类型来说不是什么问题。但是,对于像std::complex 和 std::sting这样的用户定义的数据类型来说,通过引用来传递参数会更有效。此外,因为 max() 会认为其参数是不会被改变的,我们应该将 t1和t2声明为 const (常量)。下面是 max() 的改进版本:

template <class T>
T max(const T& t1, const T& t2)
{
return (t1 > t2) ? t1 : t2;
}

十一、命名空间

在C语言中不允许同一个函数在同一个程序中,会引发冲突,C++为了解决者问题引出了命名空间
定义:

namespace demo {
void test1(int i){
    cout << "hellow world" <<endl;
}
}

namespace demo1 {
void test1(int i){
    cout << "hellow world 1" <<endl;
}
}

通过 namespace 关键字修饰,这样就可以在不同得命名空间中定义相同的函数。
当使用时:

//using namespace demo1;
//     test1(6);
demo1::test1(6);

通过using namespace 指定使用哪个命名空间的函数,这样就不会有冲突。
而当你没有using namespace时,为了区分你使用的哪个命名空间的函数,必须在其函数签名加上:
命名空间的名字加上:,例如 demo1::test1(6);

十二、类的使用

前面我们比较了C++类与C语言结构体,那么现在看看类的大概使用,其实和java是一样的,
定义:

/**
int age;
class people{
private:
    int age;
public:
   void setAge(int age){
//        age=age;
        ::age=age;// :: 访问的是全局变量或者全局函数
    }
    int getAge(){
        return age;
    }
};
*/

使用:

 /**
    people p;
    p.setAge(10);
    cout << p.getAge() <<endl;
    */
    people *p = new people();
    p->setAge(30);
    cout << p->getAge() <<endl;

当你是通过new的形式创建的对象时,通过 -> 符号来调用函数

十三、析构函数

13.1、简介

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

13.2、定义

class T
{
   public:
    ~T()};
    T::~T()
{
    //函数体
};

析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

1.通过此种方式构建对象,当函数结束便会立即执行析构函数。

man m(tom);   //栈里构建对象

2.通过此种方式构建对象,当函数结束不会立即执行析构函数,需要等待调用delete 关键的调用 。

//    man* m=new man(); //堆里构建对象
//    delete m;

十四、构造函数的初始化成员列表

14.1、定义

class foo
{
public:
foo(string s, int i):name(s), id(i){} ; // 初始化列表
private:
string name ;int id ;
};

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。

14.2、好处
我们通过一个例子来看下:

class Test1
{
public:
Test1() // 无参构造函数
{cout << "Construct Test1" << endl ;}
Test1(const Test1& t1) // 拷贝构造函数
{cout << "Copy constructor for Test1" << endl ;this->a = t1.a ;}
Test1& operator = (const Test1& t1) // 赋值运算符
{cout << "assignment for Test1" << endl ;this->a = t1.a ;return *this;}
int a ;
};

class Test2
{
public:
Test1 test1 ;
Test2(Test1 &t1)
{test1 = t1 ;}
};

调用代码:

Test1 t1 ;
Test2 t2(t1) ;

输出:

Construct Test1
Construct Test1
assignment for Test1

解释一下:
第一行输出对应调用代码中第一行,构造一个Test1对象
第二行输出对应Test2构造函数中的代码,用默认的构造函数初始化对象test1 // 这就是所谓的初始化阶段
第三行输出对应Test2的赋值运算符,对test1执行赋值操作 // 这就是所谓的计算阶段

那么我们将class Test2优化成初始化成员列表的形式:

class Test2
{
public:
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}

使用同样的调用代码,输出结果如下:

Construct Test1
Copy constructor for Test1

第一行输出对应 调用代码的第一行 第二行输出对应Test2的初始化列表,直接调用拷贝构造函数初始化test1,省去了调用默认构造函数的过程。

所以一个好的原则是,能使用初始化列表的时候尽量使用初始化列表

14.3、使用场景

  • 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  • 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  • 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

十五、拷贝构造函数

15.1、概览

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。

15.2、定义

如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会自动生成一个来进行对象之间非static成员的位拷贝(Bitwise Copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对成员的关联方式。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数,除非另外一个构造函数在类初始化或构造列表的时候被调用。

那么现实的如何定义?如下:

class CExample
{
    public:
        CExample(){pBuffer=NULL;nSize=0;}
        ~CExample(){delete[] pBuffer;}
        CExample(const CExample&);//拷贝构造函数i
        void init(int n){pBuffer= new char[n];nSize=n;}
    private:
        char* pBuffer;//类的对象中包含指针,指向动态分配的内存资源
        int nSize;
};
CExample::CExample(const CExample& RightSides)//拷贝构造函数的定义
{
    nSize=RightSides.nSize;//复制常规成员
    pBuffer=newchar[nSize];//分配内存
    memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}

从以上定义可以分析出拷贝够着有如下特点:

  • 拷贝构造函数必须以引用的形式传递(参数为引用值)

其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。

  • 建议使用const关键字

拷贝构造函数的格式为:类名(const 类名& 对象名);//拷贝构造函数的原型,参数是常量对象的引用。由于拷贝构造函数的目的是成员复制,不应修改原对象,所以建议使用const关键字。

  • 解决指针公用问题
class CExample
{
    public:
        CExample(){pBuffer=NULL;nSize=0;}
        ~CExample(){delete[] pBuffer;}
        void init(int n){pBuffer= new char[n];nSize=n;}
    private:
        char* pBuffer;//类的对象中包含指针,指向动态分配的内存资源
        int nSize;
};
//这个类的主要特点是包含指向其他资源的指针,pBuffer指向堆中动态分配的一段内存空间。
int main(int argc,char* argv[])
{
CExample theObjone;
theObjone.init(40);
//现在需要另一个对象,并将它初始化为theObjone
CExample theObjtwo=theObjone;
...
}

其完成方式是内存拷贝,复制所有成员的值。完成后,theObjtwo.pBuffer==theObjone.pBuffer。
即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。所以需要采用必要的手段来避免此类情况:可以在构造函数中添加操作来解决指针成员的这种问题。

那么通过析构函数可以解决这一问题:

class CExample
{
    public:
        CExample(){pBuffer=NULL;nSize=0;}
        ~CExample(){delete[] pBuffer;}
        CExample(const CExample&);//拷贝构造函数
        void init(int n){pBuffer= new char[n];nSize=n;}
    private:
        char* pBuffer;//类的对象中包含指针,指向动态分配的内存资源
        int nSize;
};
CExample::CExample(const CExample& RightSides)//拷贝构造函数的定义
{
    nSize=RightSides.nSize;//复制常规成员
    pBuffer=newchar[nSize];//分配内存
    memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}

这样,定义新对象,并用已有对象初始化新对象时,CExample(const CExample& RightSides)将被调用,而已有对象用别名RightSides传给构造函数,以用来作复制。通过析构重新给指针分配内存,已达到不共用的效果。

15.3、使用场景

  • 一个对象作为函数参数,以值传递的方式传入函数体;
  • 一个对象作为函数返回值,以值传递的方式从函数返回;
  • 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);

15.4、赋值重载
下面的代码与上例相似

int main(int argc,char* argv[])
{
     CExample theObjone;
    theObjone.init(40);
    CExample theObjthree;
    theObjthree.init(60);
    //现在需要一个对象赋值操作,被赋值对象的原内容被清除,并用右边对象的内容填充。
    theObjthree=theObjone;
    return 0;
}

这里用到了”=”号,”=”号大多数表示初始化。更多时候,这种初始化也可用圆括号表示。例如:CExample theObjthree(theObjone);。

而本例子中,”=”表示赋值操作。将对象theObjone的内容复制到对象theObjthree,这其中涉及到对象theObjthree原有内容的丢弃,新内容的复制。
但”=”的缺省操作只是将成员变量的值相应复制。由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放。指针的值被复制了,但指针所指内容并未被复制。
因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载”=”赋值操作符号。

重载的示例
类定义变为:

class CExample
{
    public:
        CExample(){pBuffer=NULL;nSize=0;}
        ~CExample(){delete pBuffer;}
        CExample(const CExample&);//拷贝构造函数
        CExample& operator=(const CExample&);//赋值符重载
        void init(int n){pBuffer= new char[n];nSize=n;}
    private:
        char* pBuffer;//类的对象中包含指针,指向动态分配的内存资源
        int nSize;
};
//赋值操作符重载
CExample& CExample::operator=(const CExample& RightSides)
{
    if(this==&RightSides)//如果自己给自己赋值则直接返回
        {return *this;}
    nSize=RightSides.nSize;//复制常规成员
    char* temp=new char[nSize];//复制指针指向的内容
    memcpy(temp,RightSides.pBuffer,nSize*sizeof(char));
    delete[] pBuffer;//删除原指针指向内容(将删除操作放在后面,避免X=X特殊情况下,内容的丢失)
    pBuffer=temp;//建立新指向
    return *this;
}

十六、explicit 关键字

16.1、概览

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

16.2、作用
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。

  1. 是个构造器 ,
  2. 是个默认且隐含的类型转换操作符。

    例如:

class Test{
     Test(Test t);
}
int main(int argc,char* argv[])
{
   Test t1;
   Test t2=t1;
   return 0;
}

如果这个时候我们没有使用explicit 修饰,编译器会默认以为我们想要调用Test类的构造函数,其实我们只想将T2也只想T1的指针。这样就违背了我们的本意,
这时候就要在这个构造器前面加上explicit修饰,指定这个构造器只能被明确的调用/使用, 不//能作为类型转换操作符被隐含的使用

class Test1
{
public:
    Test1(int n)
    {
        num=n;
    }//普通构造函数
private:
    int num;
};
class Test2
{
public:
    explicit Test2(int n)
    {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};
int main()
{
    Test1 t1=12;//隐式调用其构造函数,成功
    Test2 t2=12;//编译错误,不能隐式调用其构造函数
    Test2 t2(12);//显式调用成功
    return 0;
}

Test1的构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。
普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。

十七、this指针

this 就是只想自己实例的指针

十八、static关键字的使用

18.1、注意

  1. static静态变量是放在静态内存去的,程序加载就存在,一直到程序退出才清理
  2. 类的静态成员变量必须初始化:
int man::count=0;//类静态成员变量初始化的方式

3、类的static成员和类的对象没有直接关系,类的静态成员是放到静态内存去的,程序开始执行就存在,程序退出才清理
4. 类的静态成员变量不管类的实例有多少,但成员变量只有一份
5. 类的静态函数内部不能直接访问类的动态成员变量

未完待续……

猜你喜欢

转载自blog.csdn.net/qq_33750826/article/details/79950327