语言特性


语言特性

常量指针和指针常量的区别

常量指针

常量指针本质上是一个指针,只不过这个指针指向的对象是常量。

特点:const的位置在指针声明运算符 *的左侧。只要const位于 * 的左侧,无论他在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)

const int * p;
int const *p;

注意1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。

例如:

#include<iostream>
using namespace std;
int main(){
    const int c_var  = 8;
    const int *p = &c_var;
    *p = 6; //error assignment of read-only location '*p'
    return 0;
}

注意2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此可以被重新赋值。

例如:

#include<iostream>
using namespace std;
int main(){
    const int c_var1 = 8;
    const int c_var2 = 8;
    const int *p = &c_var1;
    p = &c_var2;  //常量指针p是地址
    return 0;
}

指针常量

指针常量的本质上是一个常量,只不过这个常量的值是一个指针。

特点:const位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示指针指向的类型 ,即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。

const int var;
int * const c_p= &var;

注意1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。

#include<iostream>
using namespace std;
int main(){
    int var,var1;
    int *const c_p = &var;
    *c_p = &var1;   //error assignment of read_only variable 'c_p'
    return 0;
}

注意2: 指针的内容可以改变

#include<iostream>
using namespace std;
int main(){
    int var= 3;
    int * const c_var = &var;
    *c_var = 12;
    return 0;
}

函数指针和指针函数的区别

指针函数

指针函数本质上是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。

#include<iostream>
using namespace std;
struct Type{
    int var1;
    int var2;
};
Type * fun(int tmp1,int tmp2){
    Type *t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}
int main(){
    Type *t = fun(5,6);
    return 0;
}

函数指针

函数指针本质上是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。

举例:

#include<iosteam>
using namespace std;
int fun1(int tmp1,int tmp2){
    return tmp1 * tmp2;
}
int fun2(int tmp1,int tmp2){
    return tmp1/tmp2;
}
int main(){
    int (*fun)(int x,int y);
    fun = fun1;
    cout<<fun(15,5)<<endl;
    fun = fun2;
    cout<<fun(15,5)<<endl;
    return 0;
}

区别

  • 本质不同

    指针函数本质上是一个函数,其返回值为指针。

    函数指针本质上是一个指针变量,其值指向一个函数。

  • 定义形式不同

    指针函数:

    int  * fun(int tmp1,int tmp2) ;

    这里 * 表示函数返回值类型是指针类型函数指针:

    int (*fun)(int tmp1,int tmp2);

    这里的 * 表示变量本身是指针类型。

左值和右值

左值:指表达式结束后依然存在的持久对象。

右值:表达式结束就不再存在的临时对象。

左值和右值的区别:左值持久,右值短暂

右值引用和左值引用的区别:

左值引用不能绑定到要转换的表达式、字面常量或返回右值的表达式。右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。

#include <iostream>
using namespace std;

void fun1(int& tmp) 
{ 
  cout << "fun1(int& tmp):" << tmp << endl; 
} 

void fun2(int&& tmp) 
{ 
  cout << "fun2(int&& tmp)" << tmp << endl; 
} 

int main() 
{ 
  int var = 11; 
  fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
  fun1(var);
  fun2(1); 
}

move()函数的实现原理

move()两个作用:

独享指针所有权的转移

左值到右值属性的转移

template<typename T>
typename remove_reference<T>::type && move(T&&t){
    return static_cast<typename_reference<T>::type &&>(t);
}

说明:引用折叠原理

右值传递给上述的形参T&& 依然是右值,即T&& && 相当于 T&&

左值传递给上述的形参T&&依然是左值,即T&& &相当于T&

小结:通过引用折叠原理可以知道,move()函数的形参既可以左值也可以是右值。

remove_reference具体实现:

//原始的,最通用版本
template<typename T>struct remove_reference{
    typedef T type;
}
//部分版本特例化 将用于左值引用和右值引用
template<class T>struct remove_reference<T&>  //左值引用
{
    typedef T type;
}
template<class T> struct remove_reference<T&&> //右值引用
{
    typedef T  type;  
}
//举例如下 下列定义的a,b,c三个变量都是int类型
int i;
remove_reference<decltype(42)>::type a;  //使用原版本
remove_reference<decltype(i)>::type a;  //使用左值引用
remove_reference<decltype(std::move(i))>::type b;  //使用右值引用特例版本
//decltype 类似auto 可以进行类型推导

举例:

int var = 10;
//转化过程
1.std::move(var) =>std::move(int && &)=>折叠后std::move(int &)
2.此时:T类型为int & ,typename remove_reference<T>::type 为 int,这里使用remove_reference 的左值引用的特例化版本
3.通过static_cast 将int & 强制转换为int &&
整个std::move被实例化如下:
    
int &&move(int &t){
    return static_cast<int &&>(t);
}

总结:std::move()实现原理:

  • 利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。
  • 然后通过remove_reference移除引用,得到具体的类型T
  • 最后通过static_cast进行强制类型转换,返回T&&右值引用

指针的大小和用法

指针:只想另外一种类型的复合类型

指针的大小:在64位计算机中,指针占8个字节空间

#include<iostream>
using namespace std;
int main(){
    int *p = nullptr;
    cout<<sizeof(p)<<endl; //8
    char *p1 =nullptr;
    cout<<sizeof(p1)<<endl; //8
    return 0;
}

指针的用法

1.指向普通对象的指针

#include<iostream>
using namespace std;
class A{
    
};
int main(){
    A * p = new A();
    return 0;
}

2.指向常量对象的指针,常量指针

#include<iostream>
using namespace std;
int main(){
    const int c_var = 10;
    const int *p = &c_var;
    cout<<*p<<endl;
    return 0;
}

3.指向函数的指针:函数指针

#include<iostream>
using namespace std;
int add(int a,int b){
    return a+b;
}
int main(void){
    int (*fun_p)(int,int);
    fun_p = add;
    cout<<fun_p(1,6)<<endl;
}

4.指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。特别注意:定义指向成员函数的指针时,要标明指针所属的类。

#include<iostream>
using namespace std;
class A{
public:
    int var1,var2;
    int add(){
        return var1 * var2;
    } 
};
int main(){
    A ex;
    ex.var1 = 3;
    ex.var2 = 4;
    int *p = &ex.var1; //指向对象成员变量的指针
    cout<<*p<<endl;
    
    int (A::*fun_p)();
    fun_p = &A::add; //指向对象成员函数的指针fun_p
    cout<< (ex.*fun_p)()<<endl; //函数指针 
    return 0;
}

5.this指针:指向类的当前对象的指针常量

#include<iostream>
#include<cstring>
using namespace std;
class A{
public:
    void set_name(string tmp){
        this->name = tmp;
    }
    void set_age(int tmp){
        this->age  = tmp;
    }
    void set_sex(int tmp){
        this->sex = tmp;
    }
    void show(){
        cout<<"Name : "<<this->name<<endl;
        cout<<"Age : "<<this->age<<endl;
        cout<<"Sex : "<<this->sex<<endl;
    }
private:
    string name;
    int age;
    int sex;
};
int main(){
    A *p = new A();
    p->set_name("Alice");
    p->set_age(16);
    p->set_sex(1);
    p->show();
    return 0;
}

悬空指针和野指针

悬空指针:

若指针指向一块内存空间,当这块内存空间被释放后,该指针仍然指向这块内存空间,此时称该指针为悬空指针。

举例:

void *p = malloc(size);
free(p);
//此时p指向的内存空间已经被释放,p就是悬空指针

野指针:

“野指针”是指不确定其指向的指针,未初始化的指针为野指针。

void *p;

强制类型转换

static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。

  1. 用于基本数据类型的转换
  2. 用于类层次之间的基类和派生类之间指针或者引用的转换(不要求必须包含虚函数,但是必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的进行下行转换(基类的指针或引用转换成派生类表示),由于没有动态类型检查,所以是不安全的,最好用dynamic_cast进行下行转换
  3. 可以将空指针转化为目标类型的空指针
  4. 可以将任何类型的表达式转化为void类型

const_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针和引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。

reinterpret_cast:改变指针或引用的类型,将指针或引用转换为一个足够长度的整型,将整型化为指向或引用类型。

dynamic_cast:

  • 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
  • 只能用于带有虚函数的基类或派生类的指针或引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回NULL,不能用于基本数据类型的转换
  • 向上进行转换时,即派生类类的指针转换成基类类的指针和static_cast效果是一样的。(这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)

上行转换永远是安全的,下行转换建议使用dynamic_cast.

判断结构体是否相等

需要重载操作符 == 判断两个结构体是否相等 ,不能用函数memcmp来判断两个结构体是否相等,因为memcmp函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法进行比较。

利用运算符重载来实现结构体对象的比较.

#include<iostream>
using namespace std;
struct A
{
    char c;
    int val;
    A(char c_tmp,int tmp):c(c_tmp),val(tmp){}
    friend bool operator==(const A &tmp1, const A &tmp2);
    //友元运算符重载函数
};

bool operator==(const A &tmp1,const A &tmp2){
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main(){
    A ex1('a', 90), ex2('b', 80);
    if(ex1 == ex2){
        cout << "ex1==ex2" << endl;
    }
    else{
        cout << "ex1!=ex2" << endl;
    }
    return 0;
}

参数传递

参数传递的三种方式:

  • 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
  • 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而修改它所指向的对象的值。
  • 引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。
#include<iostream>
using namespace std;
void fun1(int tmp){
    cout << &tmp << endl;
}

void fun2(int *tmp){
    cout << tmp << endl;
}

void fun3(int &tmp){
    cout << &tmp << endl;
}
int main(){
    int var = 5;
    cout << "var 在主函数中的地址:" << &var << endl;
    cout << "var 值传递时的地址:";
    fun1(var);
    cout << "var 指针传递时的地址:";
    fun2(&var);
    cout << "var 引用传递时的地址:";
    fun3(var);
    return 0;
}

说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样。在函数体内操作的不是变量本身,引用传递和指针传递,在函数体内操作的时变量本身。

模板实现

模板

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板

实现方式:模板定义以关键字template开始,后跟一个模板参数列表。

模板参数列表不能为空

模板类型参数前必须使用关键字class或者typename,在模板参数列表中这两个关键字含义相同,可互换使用。

template<typename T,typename U...>

函数模板:通过定义一个函数模板,可以避免每一种类型定义一个新函数。

  • 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。
#include<iostream>
using namespace std;

template<typename T>
T add_fun(const T &tmp1,const T &tmp2){
    return tmp1 + tmp2;
}
int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);

    // cout << add_fun(var1, var3); 两者类型不同 出错
    return 0;
}

类模板:类似函数模板,类模板以关键字template开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板后面的尖括号中指明类型。

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
public:
    //构造函数
    Complex(T a, T b)
    {
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}

函数模板和类模板的区别

实例化方式不同 :函数模板实例化编译程序在处理函数调用时自动完成类模板实例化需要在程序中显式指定。

实例化的结果不同:函数模板实例化是一个函数。类模板实例化后是一个类

默认参数:类模板在模板参数列表中可以有默认参数

特化:函数模板只能全特化 ,而类模板可以全特化也可以偏特化。

调用方式不同:函数模板可以隐式调用,也可以显式调用,类模板只能显式调用

函数模板调用方式举例:

#include<iostream>
using namespace std;
template<typename T>
T add_fun(const T &tmp1,const T &tmp2){
    return tmp1 + tmp2;
}
int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun<int>(var1, var2); //显式调用

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);  //隐式调用
    return 0;
}

可变参数模板

可变参数模板:接受可变数目参数的模板函数或模板类


评论
  目录