精华内容
下载资源
问答
  • Java函数式编程详解

    万次阅读 多人点赞 2019-05-05 21:46:49
    Java从1.8以后引入了函数...使用Consumer作为示例,它是一个函数式接口,包含一个抽象方法accept,这个方法只有输入而无输出也就是说这个方法无返回值。 现在我们要定义一个Consumer接口的实例化对象,传统的方式是...

    Java从1.8以后引入了函数式编程,这是很大的一个改进。函数式编程的优点在提高编码的效率,增强代码的可读性。本文历时两个多月一点点写出来,即作为心得,亦作为交流。

    1.Java函数式编程的语法:

    使用Consumer作为示例,它是一个函数式接口,包含一个抽象方法accept,这个方法只有输入而无输出也就是说这个方法无返回值。 
    现在我们要定义一个Consumer接口的实例化对象,传统的方式是这样定义的:

    public static void main(String[] args) {
            //JDK1.8版本之前的做法
            Consumer<Integer> con = new Consumer<Integer>() {
                @Override
                public void accept(Integer t) {
                    System.out.println(t);
                }
            };
            //调用方法
            con.accept(3);
        }

    这里可以打印出3.

    上面是JDK1.8以前的做法,在1.8以后引入函数式的编程可以这样写:

    public static void main(String[] args) {
            //第一种写法:
            Consumer<Integer> con = (param) -> {System.out.println(param);}; 
            //第二种写法:
            Consumer<Integer> con1 = (param) -> System.out.println(param);
            //第三种写法:
            Consumer<Integer> con2 = System.out::println;
            
            con2.accept(3);
        }

    上面的con、con1、con2这个Consumer对象的引用照样可以打印出3.

    在上面的第二种写法是第一种写法的精简,但是只有在函数题中只有一条语句时才可以这么使用,做进一步的简化。

    在上面的第三种写法是对第一、二种写法的进一步精简,由于第三种写法只是进行打印,调用了System.out中的println静态方法对输入参数直接进行打印。它表示的意思就是针对输入的参数将其调用System.out中的静态方法println进行打印。

    上面已说明,函数式编程接口都只有一个抽象方法,因此在采用这种写法时,编译器会将这段函数编译后当作该抽象方法的实现。 
    如果接口有多个抽象方法,编译器就不知道这段函数应该是实现哪个方法的了。 
    因此,=  后面的函数体我们就可以看成是accept函数的实现。

    输入:-> 前面的部分,即被()包围的部分。此处只有一个输入参数,实际上输入是可以有多个的,如两个参数时写法:(a, b);当然也可以没有输入,此时直接就可以是()。
    函数体:->后面的部分,即被{}包围的部分;可以是一段代码。
    输出:函数式编程可以没有返回值,也可以有返回值。如果有返回值时,需要代码段的最后一句通过return的方式返回对应的值。但是Consumer这个函数式接口不能有具体的返回值。

    Java 8 中我们可以通过 `::` 关键字来访问类的构造方法,对象方法,静态方法。

    好了,到这一步就可以感受到函数式编程的强大能力。 
    通过最后一段代码,我们可以简单的理解函数式编程,Consumer接口直接就可以当成一个函数了,这个函数接收一个输入参数,然后针对这个输入进行处理;当然其本质上仍旧是一个对象,但我们已经省去了诸如老方式中的对象定义过程,直接使用一段代码来给函数式接口对象赋值。 
    而且最为关键的是,这个函数式对象因为本质上仍旧是一个对象,因此可以做为其它方法的参数或者返回值,可以与原有的代码实现无缝集成!


    2.Java函数式接口

    java.util.function.Consumer;
    java.util.function.Function;
    java.util.function.Predicate;

    2.1 Consumer是一个函数式编程接口; 顾名思义,Consumer的意思就是消费,即针对某个东西我们来使用它,因此它包含有一个有输入而无输出(无返回值)的accept接口方法; 
    除accept方法,它还包含有andThen这个方法; 

    JDK源码定义如下:

    default Consumer<T> andThen(Consumer<? super T> after) {
            Objects.requireNonNull(after);
            return (T t) -> { accept(t); after.accept(t); };

     }

    andThen这个方法是作用是:指定在调用当前Consumer后是否还要调用其它的Consumer; 

    public static void main(String[] args) {
            //定义第一个Consumer
            Consumer<Integer> consumer1 = (param) -> {System.out.println(param);};
            
            //定义第二个Consumer
            Consumer<Integer> consumer2 = (param) -> {System.out.println(param * param);};
            
            //consumer1可以连续的调用自己
            //consumer1.andThen(consumer1).andThen(consumer1).accept(3);
            //打印出 3 3 3
            
            //consumer1可以调用自己后调用consumer2
            consumer1.andThen(consumer1).andThen(consumer2).accept(3);
            //打印出3 3 9
        }

    注意:当一个Consumer接口调用另外一个Consumer对象时两个Counsumer对象的泛型必须一致。

    2.2  Function也是一个函数式编程接口;它代表的含义是“函数”,它是个接受一个参数并生成结果的函数,而函数经常是有输入输出的,因此它含有一个apply方法,包含一个输入与一个输出; 
    除apply方法外,它还有compose与andThen及indentity三个方法,其使用见下述示例;

    apply的用法:

    public static void main(String[] args) {
            Function<Integer, Integer> fun = res -> res + 1;
            Integer  i = fun.apply(2);
            System.out.println(i);
    }

    上面打印出2。

    Function编程接口有两个泛型Function<T, R> T表示:函数的输入类型,R表示:函数的输出类型。

    compose的用法:

    JDK源码定义:

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
            Objects.requireNonNull(before);
            return (V v) -> apply(before.apply(v));

     }

    public static void main(String[] args) {
            Function<Integer, Integer> fun = res -> res + 1;
            Function<Integer, Integer> fun1 = res -> res * 10;
            Integer i = (Integer) fun.compose(fun1).apply(2);
            System.out.println(i);
    }

    上面打印出21;

    上面表示了fun2接收到2以后先计算,然后将fun2计算的结果再交给fun再计算。

    andThen方法的用法:

    JDK源码定义:

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return (T t) -> after.apply(apply(t));

     }

    public static void main(String[] args) {
            Function<Integer, Integer> fun = res -> res + 1;
            Function<Integer, Integer> fun1 = res -> res * 10;
            Integer i = (Integer) fun.andThen(fun1).apply(3);
            System.out.println(i);
     }

    上面打印出40

    上面表示了fun先计算得到4,然后将计算结果4再给fun1计算得到40;

    indentity方法的用法:

    JDK源码定义:

    static <T> Function<T, T> identity() {
            return t -> t;
     }

    public static void main(String[] args) {
            System.out.println(Function.identity().apply("总分"));

     }

    上面打印出“总分“;就是说传入什么参数输出什么参数。

    2.3 Predicate为函数式接口,predicate的中文意思是“断定”,即判断的意思,判断某个东西是否满足某种条件; 因此它包含test方法,根据输入值来做逻辑判断,其结果为True或者False。 

    test方法的用法:

    JDK源码定义: boolean test(T t);

    public static void main(String[] args) {
            Predicate<String> pre = res -> res.equals("1234");
            boolean rest = pre.test("1234");
            System.out.println(rest);
     }

    上面打印出true。

    and方法的用法:

    JDK源码定义:

    default Predicate<T> and(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) && other.test(t);
     }

    public static void main(String[] args) {
            Predicate<String> pre = res -> res.equals("1234");
            Predicate<String> pre1 = res -> res.equals("1234");
            Predicate<String> pre3 = res -> res.equals("1234");
            boolean rest = pre.and(pre1).test("1234");
            boolean rest1 = pre.and(pre1).test("12341");
            System.out.println(rest);
            System.out.println(rest1); 

    }
    上面先打印出true,后打印出false

    根据源码,"1234"先和pre比较,如果为true,再和pre1比较。不为true直接返回false,不再和pre1比较。当“1234”和pre比较后为true,再和pre1比较结果为true,所以最终返回true。

    negate()的用法:

    JDK源码定义:

    default Predicate<T> negate() {
            return (t) -> !test(t);
     }

    public static void main(String[] args) {
            Predicate<String> pre = res -> res.equals("1234");
            boolean rest = pre.negate().test("1234");
            System.out.println(rest);
    }

    上面打印出false,上面的意思是如果比较的结果是true,那么返回false,如果为false,就返回true。

    or的方法的用法:

    JDK源码定义:

    default Predicate<T> or(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) || other.test(t);
    }

    public static void main(String[] args) {
            Predicate<String> pre = res -> res.equals("1234");
            Predicate<String> pre1 = res -> res.equals("12341");
            boolean rest = pre.or(pre1).test("12341");
            System.out.println(rest);
     }

    上面打印出true;

    or方法的意思是:只要一个为true就返回true。

    isEqual方法用法:

    JDK源码定义:

    static <T> Predicate<T> isEqual(Object targetRef) {
            return (null == targetRef)
                    ? Objects::isNull
                    : object -> targetRef.equals(object);
     }

    public static void main(String[] args) {
            System.out.println(Predicate.isEqual("12345").test("12345"));

    }

    上面打印出true;isEqual里的参数是要比较的目标对象。

    3.函数式编程接口的使用

    通过Stream以及Optional两个类,可以进一步利用函数式接口来简化代码。

    3.1 Stream

    Stream可以对多个元素进行一系列的操作,也可以支持对某些操作进行并发处理。

    3.1.1 Stream对象的创建

    Stream对象的创建途径有以下几种:

    a. 创建空的Stream对象

    Stream str = Stream.empty();

    b. 通过集合类中的stream或者parallelStream方法创建;

    List<String> list = Arrays.asList("a", "b", "c", "d");
    Stream listStream = list.stream(); //获取串行的Stream对象
    Stream parallelListStream = list.parallelStream(); //获取并行的Stream对象  

    c. 通过Stream中的of方法创建:

    Stream s = Stream.of("test");

    Stream s1 = Stream.of("a", "b", "c", "d");

    d. 通过Stream中的iterate方法创建: 
        iterate方法有两个不同参数的方法:

    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f);  
    public static<T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
    其中第一个方法将会返回一个无限有序值的Stream对象:它的第一个元素是seed,第二个元素是f.apply(seed); 第N个元素是f.apply(n-1个元素的值);生成无限值的方法实际上与Stream的中间方法类似,在遇到中止方法前一般是不真正的执行的。因此无限值的这个方法一般与limit等方法一起使用,来获取前多少个元素。 
    当然获取前多少个元素也可以使用第二个方法。 
    第二个方法与第一个方法生成元素的方式类似,不同的是它返回的是一个有限值的Stream;中止条件是由hasNext来断定的。
    第二种方法的使用示例如下:

    /**
     * 本示例表示从1开始组装一个序列,第一个是1,第二个是1+1即2,第三个是2+1即3..,直接10时中止;
     * 也可简化成以下形式:
     *        Stream.iterate(1,
     *        n -> n <= 10,
     *        n -> n+1).forEach(System.out::println);
     * 写成以下方式是为简化理解
     */
    Stream.iterate(1,
            new Predicate<Integer>() {
                @Override
                public boolean test(Integer integer) {
                    return integer <= 10;
                }
            },
        new UnaryOperator<Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return integer+1;
            }
    }).forEach(System.out::println);

    e. 通过Stream中的generate方法创建 
    与iterate中创建无限元素的Stream类似,不过它的每个元素与前一元素无关,且生成的是一个无序的队列。也就是说每一个元素都可以随机生成。因此一般用来创建常量的Stream以及随机的Stream等。 
    示例如下:

    /**
     * 随机生成10个Double元素的Stream并将其打印
     */
    Stream.generate(new Supplier<Double>() {
        @Override
        public Double get() {
            return Math.random();
        }
    }).limit(10).forEach(System.out::println);

    //上述写法可以简化成以下写法:
    Stream.generate(() -> Math.random()).limit(10).forEach(System.out::println);


    f. 通过Stream中的concat方法连接两个Stream对象生成新的Stream对象 
    这个比较好理解不再赘述。

    3.1.2 Stream对象的使用
    Stream对象提供多个非常有用的方法,这些方法可以分成两类: 
    中间操作:将原始的Stream转换成另外一个Stream;如filter返回的是过滤后的Stream。 
    终端操作:产生的是一个结果或者其它的复合操作;如count或者forEach操作。

    其清单如下所示,方法的具体说明及使用示例见后文。 
    所有中间操作:

    所有的终端操作:

    下面就几个比较常用的方法举例说明其用法:

    3.1.2.1 filter
    用于对Stream中的元素进行过滤,返回一个过滤后的Stream 
    其方法定义如下:

    Stream<T> filter(Predicate<? super T> predicate);

    使用示例:

    Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa");
    //查找所有包含t的元素并进行打印
    s.filter(n -> n.contains("t")).forEach(System.out::println);

    3.1.2.2 map
    元素一对一转换。 
    它接收一个Funcation参数,用其对Stream中的所有元素进行处理,返回的Stream对象中的元素为Function对原元素处理后的结果 
    其方法定义如下:

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

    示例,假设我们要将一个String类型的Stream对象中的每个元素添加相同的后缀.txt,如a变成a.txt,其写法如下:

    Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa");
    s.map(n -> n.concat(".txt")).forEach(System.out::println);

    3.1.2.3 flatMap
    元素一对多转换:对原Stream中的所有元素使用传入的Function进行处理,每个元素经过处理后生成一个多个元素的Stream对象,然后将返回的所有Stream对象中的所有元素组合成一个统一的Stream并返回; 
    方法定义如下:

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    示例,假设要对一个String类型的Stream进行处理,将每一个元素的拆分成单个字母,并打印:

    Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa");
    s.flatMap(n -> Stream.of(n.split(""))).forEach(System.out::println);

    3.1.2.4 takeWhile
    方法定义如下:

    default Stream<T> takeWhile(Predicate<? super T> predicate)

    如果Stream是有序的(Ordered),那么返回最长命中序列(符合传入的Predicate的最长命中序列)组成的Stream;如果是无序的,那么返回的是所有符合传入的Predicate的元素序列组成的Stream。 
    与Filter有点类似,不同的地方就在当Stream是有序时,返回的只是最长命中序列。 
    如以下示例,通过takeWhile查找”test”, “t1”, “t2”, “teeeee”, “aaaa”, “taaa”这几个元素中包含t的最长命中序列:

    Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa", "taaa");
    //以下结果将打印: "test", "t1", "t2", "teeeee",最后的那个taaa不会进行打印 
    s.takeWhile(n -> n.contains("t")).forEach(System.out::println);

    3.1.2.5 dropWhile
    与takeWhile相反,如果是有序的,返回除最长命中序列外的所有元素组成的Stream;如果是无序的,返回所有未命中的元素组成的Stream;其定义如下:

    default Stream<T> dropWhile(Predicate<? super T> predicate)

    如以下示例,通过dropWhile删除”test”, “t1”, “t2”, “teeeee”, “aaaa”, “taaa”这几个元素中包含t的最长命中序列:

    Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa", "taaa");
    //以下结果将打印:"aaaa", "taaa"  
    s.dropWhile(n -> n.contains("t")).forEach(System.out::println);

    3.1.2.6 reduce与collect
    关于reduce与collect由于功能较为复杂,在后续将进行单独分析与学习,此处暂不涉及。

    3.2 Optional用于简化Java中对空值的判断处理,以防止出现各种空指针异常。 
    Optional实际上是对一个变量进行封装,它包含有一个属性value,实际上就是这个变量的值。

    3.2.1 Optional对象创建
    它的构造函数都是private类型的,因此要初始化一个Optional的对象无法通过其构造函数进行创建。它提供了一系列的静态方法用于构建Optional对象:

    3.2.1.1 empty
    用于创建一个空的Optional对象;其value属性为Null。 
    如:

    Optional o = Optional.empty();

    3.2.1.2 of
    根据传入的值构建一个Optional对象; 
    传入的值必须是非空值,否则如果传入的值为空值,则会抛出空指针异常。 
    使用:

    o = Optional.of("test"); 
    1
    3.2.1.3 ofNullable
    根据传入值构建一个Optional对象 
    传入的值可以是空值,如果传入的值是空值,则与empty返回的结果是一样的。

    3.2.2 方法
    Optional包含以下方法:

    3.2.3 使用场景
    常用的使用场景如下:

    3.2.3.1 判断结果不为空后使用
    如某个函数可能会返回空值,以往的做法:

    String s = test();
    if (null != s) {
        System.out.println(s);
    }

    现在的写法就可以是:

    Optional<String> s = Optional.ofNullable(test());
    s.ifPresent(System.out::println);

    乍一看代码复杂度上差不多甚至是略有提升;那为什么要这么做呢? 
    一般情况下,我们在使用某一个函数返回值时,要做的第一步就是去分析这个函数是否会返回空值;如果没有进行分析或者分析的结果出现偏差,导致函数会抛出空值而没有做检测,那么就会相应的抛出空指针异常! 
    而有了Optional后,在我们不确定时就可以不用去做这个检测了,所有的检测Optional对象都帮忙我们完成,我们要做的就是按上述方式去处理。

    3.2.3.2 变量为空时提供默认值
    如要判断某个变量为空时使用提供的值,然后再针对这个变量做某种运算; 
    以往做法:

    if (null == s) {
        s = "test";
    }
    System.out.println(s);

    现在的做法:

    Optional<String> o = Optional.ofNullable(s);
    System.out.println(o.orElse("test"));

    3.2.3.3 变量为空时抛出异常,否则使用
    以往写法:

    if (null == s) {
        throw new Exception("test");
    }
    System.out.println(s);


    现在写法:

    Optional<String> o = Optional.ofNullable(s);
    System.out.println(o.orElseThrow(()->new Exception("test")));
     

    展开全文
  • C++ 运算符重载

    万次阅读 多人点赞 2018-12-26 21:39:21
    前言 本文引用于“C语言中文网”,我整理出来放在博客,方便大家共同学习。所有知识点和代码均已亲测可用,如有...4. C++运算符重载为友元函数 5. C++实现可变长度的动态数组 6. C++重载&lt;&lt;和&gt;...

    前言

    本文引用于“C语言中文网”,我整理出来放在博客,方便大家共同学习。所有知识点和代码均已亲测可用,如有疑问,可提出,一起讨论学习。

    本章内容:

    1. C++运算符重载的概念和原理
    2. C++重载=(C++重载赋值运算符)
    3. C++深拷贝和浅拷贝(C++深复制和浅复制)
    4. C++运算符重载为友元函数
    5. C++实现可变长度的动态数组
    6. C++重载<<和>>(C++重载输出运算符和输入运算符)
    7. C++重载()(强制类型转换运算符)
    8. C++重载++和--(自增和自减运算符)
    9. C++运算符重载注意事项以及汇总

    1 C++运算符重载的概念和原理 

    如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。

    有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。例如,复数是可以进行四则运算的,两个复数对象相加如果能直接用+运算符完成,不是很直观和简洁吗?

    利用 C++ 提供的“运算符重载”机制,赋予运算符新的功能,就能解决用+将两个复数对象相加这样的问题。

    运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。

    运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:

    返回值类型  operator  运算符(形参表)
    {
        ....
    }

    包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。运算符可以被多次重载。

    运算符可以被重载为全局函数,也可以被重载为成员函数。一般来说,倾向于将运算符重载为成员函数,这样能够较好地体现运算符和类的关系。来看下面的例子:

    #include <iostream>
    using namespace std;
    class Complex
    {
        public:
        double real, imag;
        Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
        Complex operator - (const Complex & c);
    };
    Complex operator + (const Complex & a, const Complex & b)
    {
        return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象
    }
    Complex Complex::operator - (const Complex & c)
    {
        return Complex(real - c.real, imag - c.imag); //返回一个临时对象
    }
    int main()
    {
        Complex a(4, 4), b(1, 1), c;
        c = a + b; //等价于 c = operator + (a,b);
        cout << c.real << "," << c.imag << endl;
        cout << (a - b).real << "," << (a - b).imag << endl; //a-b等价于a.operator - (b)
        return 0;
    }

    程序的输出结果是:

    5,5
    3,3

    程序将+重载为一个全局函数(只是为了演示这种做法,否则重载为成员函数更好),将-重载为一个成员函数。

    运算符重载为全局函数时,参数的个数等于运算符的目数(即操作数的个数);运算符重载为成员函数时,参数的个数等于运算符的目数减一。

    如果+没有被重载,第 21 行会编译出错,因为编译器不知道如何对两个 Complex 对象进行+运算。有了对+的重载,编译器就将a+b理解为对运算符函数的调用,即operator+(a,b),因此第 21 行就等价于:

    c = operator+(a, b);

    即以两个操作数 a、b 作为参数调用名为operator+的函数,并将返回值赋值给 c。

    第 12 行,在 C++ 中,“类名(构造函数实参表)”这种写法表示生成一个临时对象。该临时对象没有名字,生存期就到包含它的语句执行完为止。因此,第 12 行实际上生成了一个临时的 Complex 对象作为 return 语句的返回值,该临时对象被初始化为 a、b 之和。第 16 行与第 12 行类似。

    由于-被重载为 Complex 类的成员函数,因此,第 23 行中的a-b就被编译器处理成:

    a.operator-(b);

    由此就能看出,为什么运算符重载为成员函数时,参数个数要比运算符目数少 1 了。

    2 C++重载=(C++重载赋值运算符)

    赋值运算符=要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望=两边的操作数的类型即使不兼容也能够成立,这就需要对=进行重载。C++ 规定,=只能重载为成员函数。来看下面的例子。

    要编写一个长度可变的字符串类 String,该类有一个 char* 类型的成员变量,用以指向动态分配的存储空间,该存储空间用来存放以\0结尾的字符串。String 类可以如下编写:

    #include <iostream>
    #include <cstring>
    using namespace std;
    class String {
    private:
        char * str;
    public:
        String() :str(NULL) { }
        const char * c_str() const { return str; };
        String & operator = (const char * s);
        ~String();
    };
    String & String::operator = (const char * s)
    //重载"="以使得 obj = "hello"能够成立
    {
        if (str)
            delete[] str;
        if (s) { //s不为NULL才会执行拷贝
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        }
        else
            str = NULL;
        return *this;
    }
    String::~String()
    {
        if (str)
            delete[] str;
    };
    int main()
    {
        String s;
        s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
        cout << s.c_str() << endl;
        // String s2 = "hello!"; //这条语句要是不注释掉就会出错
        s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
        cout << s.c_str() << endl;
        return 0;
    }

    程序的运行结果:

    Good Luck,
    Shenzhou 8!

    第 8 行的构造函数将 str 初始化为 NULL,仅当执行了 operator= 成员函数后,str 才会指向动态分配的存储空间,并且从此后其值不可能再为 NULL。在 String 对象的生存期内,有可能从未执行过 operator= 成员函数,所以在析构函数中,在执行delete[] str之前,要先判断 str 是否为 NULL。

    第 9 行的函数返回了指向 String 对象内部动态分配的存储空间的指针,但是不希望外部得到这个指针后修改其指向的字符串的内容,因此将返回值设为 const char*。这样,假定 s 是 String 对象,那么下面两条语句编译时都会报错,s 对象内部的字符串就不会轻易地从外部被修改了 :

    char* p = s.c_str ();
    strcpy(s.c_str(), "Tiangong1");

    第一条语句出错是因为=左边是 char* 类型,右边是 const char * 类型,两边类型不匹配;第二条语句出错是因为 strcpy 函数的第一个形参是 char* 类型,而这里实参给出的却是 const char * 类型,同样类型不匹配。

    如果没有第 13 行对=的重载,第 34 行的s = "Good Luck,"肯定会因为类型不匹配而编译出错。经过重载后,第 34 行等价s.operator=("Good Luck,");,就没有问题了。

    在 operator= 函数中,要先判断 str 是否已经指向动态分配的存储空间,如果是,则要先释放那片空间,然后重新分配一片空间,再将参数 s 指向的内容复制过去。这样,对象中存放的字符串就和 s 指向的字符串一样了。分配空间时,要考虑到字符串结尾的\0,因此分配的字节数要比 strlen(s) 多 1。

    需要注意一点,即使对=做了重载,第 36 行的String s2 = "hello!";还是会编译出错,因为这是一条初始化语句,要用到构造函数,而不是赋值运算符=。String 类没有编写参数类型为 char * 的构造函数,因此编译不能通过。

    就上面的程序而言,对 operator= 函数的返回值类型没有什么特别要求,void 也可以。但是在对运算符进行重载时,好的风格是应该尽量保留运算符原本的特性,这样其他人在使用这个运算符时才不容易产生困惑。赋值运算符是可以连用的,这个特性在重载后也应该保持。即下面的写法应该合法:

    a = b = c;

    假定 a、b、c 都是 String 对象,则上面的语句等价于下面的嵌套函数调用:

    a.operator=( b.operator=(c) );

    如果 operator= 函数的返回值类型为 void,显然上面这个嵌套函数调用就不能成立。将返回值类型改为 String 并且返回 *this 可以解决问题,但是还不够好。因为,假设 a、b、c 是基本类型的变量,则

    (a =b) = c;

    这条语句执行的效果会使得 a 的值和 c 相等,即a = b这个表达式的值其实是 a 的引用。为了保持=的这个特性,operator= 函数也应该返回其所作用的对象的引用。因此,返回值类型为 String & 才是风格最好的写法。在 a、b、c 都是 String 对象时,(a=b)=c;等价于

    ( a.operator=(b) ).operator=(c);

    a.operator=(b) 返回对 a 的引用后,通过该引用继续调用 operator=(c),就会改变 a 的值。

    3 C++深拷贝和浅拷贝(C++深复制和浅复制) 

    同类对象之间可以通过赋值运算符=互相赋值。如果没有经过重载,=的作用就是把左边的对象的每个成员变量都变得和右边的对象相等,即执行逐个字节拷贝的工作,这种拷贝叫作“浅拷贝”。

    有的时候,两个对象相等,从实际应用的含义上来讲,指的并不应该是两个对象的每个字节都相同,而是有其他解释,这时就需要对=进行重载。

    上节我们定义了 String 类,并重载了=运算符,使得 char * 类型的字符串可以赋值给 String 类的对象。完整代码如下:

    #include <iostream>
    #include <cstring>
    using namespace std;
    class String {
    private:
        char * str;
    public:
        String() :str(NULL) { }
        const char * c_str() const { return str; };
        String & operator = (const char * s);
        ~String();
    };
    String & String::operator = (const char * s)
    //重载"="以使得 obj = "hello"能够成立
    {
        if (str)
            delete[] str;
        if (s) { //s不为NULL才会执行拷贝
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        }
        else
            str = NULL;
        return *this;
    }
    String::~String()
    {
        if (str)
            delete[] str;
    };
    int main()
    {
        String s;
        s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
        cout << s.c_str() << endl;
        // String s2 = "hello!"; //这条语句要是不注释掉就会出错
        s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
        cout << s.c_str() << endl;
        return 0;
    }

    对于上面的代码,如果让两个 String 对象相等(把一个对象赋值给另一个对象),其意义到底应该是什么呢?是两个对象的 str 成员变量都指向同一个地方,还是两个对象的 str 成员变量指向的内存空间中存放的内容相同?如果把 String 对象理解为存放字符串的对象,那应该是后者比较合理和符合习惯,而前者不但不符合习惯,还会导致程序漏洞。

    按照上面代码中 String 类的写法,下面的程序片段会引发问题:

    String s1, s2;
    s1 = "this";
    s2 = "that";
    s2 = s1;

    执行完上面的第 3 行后,s1 和 s2 的状态如图 1 (a) 所示,它们的 str 成员变量指向不同的存储空间。
     


                                                                                   图1:浅拷贝导致的错误

    s2=s1;执行的是浅拷贝。执行完s2=s1;后,s2.str s1.str 指向同一个地方, 如图 1 (b) 所示。这导致 s2.str 原来指向的那片动态分配的存储空间再也不会被释放,变成内存垃圾。

    此外,s1 和 s2 消亡时都会执行delete[] str;,这就使得同一片存储空间被释放两次,会导致严重的内存错误,可能引发程序意外中止。

    而且,如果执行完s1=s2;后 又执行s1 = "some";,则会导致 s2.str 也被释放。

    为解决上述问题,需要对做=再次重载。重载后的的逻辑,应该是使得执行s2=s1;后,s2.str 和 s1.str 依然指向不同的地方,但是这两处地方所存储的字符串是一样的。再次重载=的写法如下:

    String & String::operator = (const String & s)
    {
        if(str == s.str)
            return * this;
        if(str)
            delete[] str;
        if(s.str){ //s. str不为NULL才执行复制操作
            str = new char[ strlen(s.str) + 1 ];
            strcpy(str, s.str);
        }
        else
            str = NULL;
        return * this;
    }

    经过重载,赋值号=的功能不再是浅拷贝,而是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。

    程序第 3 行要判断 str==s.str,是因为要应付如下的语句:

    s1 = s1;

    这条语句本该不改变s1的值才对。s1=s1;等价于s.operator=(s1);,如果没有第 3 行和第 4 行,就会导致函数执行中的 str 和 s.str 完全是同一个指针(因为形参 s 引用了实参 s1,因此可以说 s 就是 s1)。第 8 行为 str 新分配一片存储空间,第 9 行从自己复制到自己,那么 str 指向的内容就不知道变成什么了。

    当然,程序员可能不会写s1=s1;这样莫名奇妙的语句,但是可能会写rs1=rs2;,如果 rs1 和 rs2 都是 String 类的引用,而且它们正好引用了同一个 String 对象,那么就等于发生了s1=s1;这样的情况。

    思考题:上面的两个 operator= 函数有什么可以改进以提高执行效率的地方?

    重载了两次=的 String 类依然可能导致问题。因为没有编写复制构造函数,所以一旦出现使用复制构造函数初始化的 String 对象(例如,String 对象作为函数形参,或 String 对象作为函数返回值),就可能导致问题。最简单的可能出现问题的情况如下:

    String s2;
    s2 = "Transformers";
    String s1(s2);

    s1 是以 s2 作为实参,调用默认复制构造函数来初始化的。默认复制构造函数使得 s1.str 和 s2.str 指向同一个地方,即执行的是浅拷贝,这就导致了前面提到的没有对=进行第二次重载时产生的问题。因此还应该为 String 类编写如下复制构造函数,以完成深拷贝:

    String::String(String & s)
    {
        if(s.str){
            str = new char[ strlen(s.str) + 1 ];
            strcpy(str, s.str);
        }
        else
            str = NULL;
    }
    
    最后,给出 String 类的完整代码:
    class String {
    private:
        char * str;
    public:
        String() :str(NULL) { }
        String(String & s);
        const char * c_str() const { return str; };
        String & operator = (const char * s);
        String & operator = (const String & s);
        ~String();
    };
    String::String(String & s)
    {
        if (s.str) {
            str = new char[strlen(s.str) + 1];
            strcpy(str, s.str);
        }
        else
            str = NULL;
    }
    String & String::operator = (const String & s)
    {
        if (str == s.str)
            return *this;
        if (str)
            delete[] str;
        if (s.str) { //s. str不为NULL才执行复制操作
            str = new char[strlen(s.str) + 1];
            strcpy(str, s.str);
        }
        else
            str = NULL;
        return *this;
    }
    String & String::operator = (const char * s)
    //重载"="以使得 obj = "hello"能够成立
    {
        if (str)
            delete[] str;
        if (s) { //s不为NULL才会执行拷贝
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        }
        else
            str = NULL;
        return *this;
    }
    String::~String()
    {
        if (str)
            delete[] str;
    };

    4 C++运算符重载为友元函数

    一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。

    例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+55+c这两个表达式都能解释得通。

    将+重载为 Complex 类的成员函数能解释c+5,但是无法解释5+c。要让5+c有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:

    class Complex
    {
        double real, imag;
    public:
        Complex(double r, double i):real(r), imag(i){};
        Complex operator + (double r);
        friend Complex operator + (double r, const Complex & c);
    };
    Complex Complex::operator + (double r)
    { //能解释c+5
        return Complex(real+r, imag);
    }
    Complex operator + (double r, const Complex & c)
    { //能解释5+c
        return Complex (c.real+r, c.imag);
    }

     5 C++实现可变长度的动态数组

    实践中经常碰到程序需要定义一个数组,但不知道定义多大合适的问题。按照最大的可能性定义,会造成空间浪费;定义小了则无法满足需要。

    如果用动态内存分配的方式解决,需要多少空间就动态分配多少,固然可以解决这个问题,但是要确保动态分配的内存在每一条执行路径上都能够被释放,也是一件头疼的事情。

    因此需要编写一个长度可变的数组类,该类的对象就能存放一个可变长数组。该数组类应该有以下特点:

    • 数组的元素个数可以在初始化该对象时指定。
    • 可以动态往数组中添加元素。
    • 使用该类时不用担心动态内存分配和释放问题。
    • 能够像使用数组那样使用动态数组类对象,如可以通过下标访问其元素。

    程序代码如下:

    #include <iostream>
    #include <cstring>
    using namespace std;
    class CArray
    {
    	int size; //数组元素的个数
    	int* ptr; //指向动态分配的数组
    public:
    	CArray(int s = 0); //s代表数组元素的个数
    	CArray(CArray & a);
    	~CArray();
    	void push_back(int v); //用于在数组尾部添加一个元素 v
    	CArray & operator = (const CArray & a); //用于数组对象间的赋值
    	int length() const { return size; } //返回数组元素个数
    	int & operator[](int i)
    	{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
    		return ptr[i];
    	};
    };
    CArray::CArray(int s) : size(s)
    {
    	if (s == 0)
    		ptr = NULL;
    	else
    		ptr = new int[s];
    }
    CArray::CArray(CArray & a)
    {
    	if (!a.ptr) {
    		ptr = NULL;
    		size = 0;
    		return;
    	}
    	ptr = new int[a.size];
    	memcpy(ptr, a.ptr, sizeof(int) * a.size);
    	size = a.size;
    }
    CArray::~CArray()
    {
    	if (ptr) delete[] ptr;
    }
    CArray & CArray::operator=(const CArray & a)
    { //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
    	if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
    		return *this;
    	if (a.ptr == NULL) { //如果a里面的数组是空的
    		if (ptr)
    		delete[] ptr;
    		ptr = NULL;
    		size = 0;
    		return *this;
    	}
    	if (size < a.size) { //如果原有空间够大,就不用分配新的空间
    		if (ptr)
    			delete[] ptr;
    		ptr = new int[a.size];
    	}
    	memcpy(ptr, a.ptr, sizeof(int)*a.size);
    	size = a.size;
    	return *this;
    }
    void CArray::push_back(int v)
    { //在数组尾部添加一个元素
    	if (ptr) {
    		int* tmpPtr = new int[size + 1]; //重新分配空间
    		memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
    		delete[] ptr;
    		ptr = tmpPtr;
    	}
    	else //数组本来是空的
    		ptr = new int[1];
    	ptr[size++] = v; //加入新的数组元素
    }
    int main()
    {
    	CArray a; //开始的数组是空的
    	for (int i = 0; i<5; ++i)
    		a.push_back(i);
    	CArray a2, a3;
    	a2 = a;
    	for (int i = 0; i<a.length(); ++i)
    		cout << a2[i] << " ";
    	a2 = a3; //a2 是空的
    	for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
    		cout << a2[i] << " ";
    	cout << endl;
    	a[3] = 100;
    	CArray a4(a);
    	for (int i = 0; i<a4.length(); ++i)
    		cout << a4[i] << " ";
    	return 0;
    }

    程序的输出结果为:

    0 1 2 3 4
    0 1 2 100 4

    []是双目运算符,有两个操作数,一个在里面,一个在外面。表达式 a[i] 等价于 a.operator[](i)。按照[]原有的特性,a[i]应该能够作为左值使用,因此 operator[] 函数应该返回引用。

    思考题:每次在数组尾部添加一个元素都要重新分配内存并且复制原有内容,显然效率是低下的。有什么办法能够加快添加元素的速度呢?

    6 C++重载<<和>>(C++重载输出运算符和输入运算符)

    在 C++ 中,左移运算符<<可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,<<本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。

    cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将<<重载为成员函数,而且重载了多次。为了使cout<<"Star War"能够成立,ostream 类需要将<<进行如下重载:

    ostream & ostream::operator << (const char* s)
    {
        //输出s的代码
        return * this;
    }

    为了使cout<<5;能够成立,ostream 类还需要将<<进行如下重载:

    ostream & ostream::operator << (int n)
    {
        //输出n的代码
        return *this;
    }

    重载函数的返回值类型为 ostream 的引用,并且函数返回 *this,就使得cout<<"Star War"<<5能够成立。有了上面的重载,cout<<"Star War"<<5;就等价于:

    ( cout.operator<<("Star War") ).operator<<(5);

    重载函数返回 *this,使得cout<<"Star War"这个表达式的值依然是 cout(说得更准确一点就是 cout 的引用,等价于 cout),所以能够和<<5继续进行运算。

    cin 是 istream 类的对象,是在头文件 <iostream> 中声明的。istream 类将>>重载为成员函数,因此 cin 才能和>>连用以输入数据。一般也将>>称为“流提取运算符”或者“输入运算符”。

    例题:假定 c 是 Complex 复数类的对象,现在希望写cout<<c;就能以 a+bi 的形式输出 c 的值;写cin>>c;就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。

    显然,要对<<>>进行重载,程序如下:

    #include <iostream>
    #include <string>
    #include <cstdlib>
    using namespace std;
    class Complex
    {
    	double real,imag;
    public:
    	Complex( double r=0, double i=0):real(r),imag(i){ };
    	friend ostream & operator<<( ostream & os,const Complex & c);
    	friend istream & operator>>( istream & is,Complex & c);
    };
    ostream & operator<<( ostream & os,const Complex & c)
    {
    	os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
    	return os;
    }
    istream & operator>>( istream & is,Complex & c)
    {
    	string s;
    	is >> s; //将"a+bi"作为字符串读入, "a+bi" 中间不能有空格
    	int pos = s.find("+",0);
    	string sTmp = s.substr(0,pos); //分离出代表实部的字符串
    	c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
    	sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
    	c.imag = atof(sTmp.c_str());
    	return is;
    }
    int main()
    {
    	Complex c;
    	int n;
    	cin >> c >> n;
    	cout << c << "," << n;
    	return 0;
    }

    程序的运行结果:

    13.2+133i 87
    13.2+133i,87

    因为没有办法修改 ostream 类和 istream 类,所以只能将<<>>重载为全局函数的形式。由于这两个函数需要访问 Complex 类的私有成员,因此在 Complex 类定义中将它们声明为友元。

    cout<<c会被解释成operator<<(cout, c),因此编写 operator<< 函数时,它的两个参数就不难确定了。

    第 13 行,参数 os 只能是 ostream 的引用,而不能是 ostream 对象,因为 ostream 的复制构造函数是私有的,没有办法生成 ostream 参数对象。operator<< 函数的返回值类型设为 ostream &,并且返回 os,就能够实现<<的连续使用,如cout<<c<<5。在本程序中,执行第 34 行的cout<<c进入 operator<< 后,os 引用的就是 cout,因此第 34 行就能产生输出。

    用 cin 读入复数时,对应的输入必须是 a+bi 的格式,而且中间不能有空格,如输入 13.2+33.4i。第 21 行的is>>s;读入一个字符串。假定输入的格式没有错误,那么被读入 s  的就是 a+bi 格式的字符串。

    读入后需要将字符串中的实部 a 和虚部 b 分离出来,分离的办法就是找出被+隔开的两个子串,然后将两个字符串转换成浮点数。第 24 行调用了标准库函数 atof 来将字符串转换为浮点数。该函数的原型是float atof(const char *),它在 <cstdlib> 头文件中声明。

    7 C++重载()(强制类型转换运算符)

    在 C++ 中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。

    类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名(),即变成对运算符函数的调用。

    下面的程序对 double 类型强制转换运算符进行了重载。

    #include <iostream>
    using namespace std;
    class Complex
    {
        double real, imag;
    public:
        Complex(double r = 0, double i = 0) :real(r), imag(i) {};
        operator double() { return real; } //重载强制类型转换运算符 double
    };
    int main()
    {
        Complex c(1.2, 3.4);
        cout << (double)c << endl; //输出 1.2
        double n = 2 + c; //等价于 double n = 2 + c. operator double()
        cout << n; //输出 3.2
    }

    程序的输出结果是:

    1.2
    3.2

    第 8 行对 double 运算符进行了重载。重载强制类型转换运算符时,不需要指定返回值类型,因为返回值类型是确定的,就是运算符本身代表的类型,在这里就是 double。

    重载后的效果是,第 13 行的(double)c等价于c.operator double()

    有了对 double 运算符的重载,在本该出现 double 类型的变量或常量的地方,如果出现了一个 Complex 类型的对象,那么该对象的 operator double 成员函数就会被调用,然后取其返回值使用。

    例如第 14 行,编译器认为本行中c这个位置如果出现的是 double 类型的数据,就能够解释得通,而 Complex 类正好重载了 double 运算符,因而本行就等价于:

    double n = 2 + c.operator double();

    8 C++重载++和--(自增和自减运算符)

    自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。

    ++为例,假设 obj 是一个 CDemo 类的对象,++objobj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:

    CDemo & CDemo::operator ++ ()
    {
        //...
        return * this;
    }

    那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。

    为了解决这个问题,C++ 规定,在重载++--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:

    #include <iostream>
    using namespace std;
    class CDemo {
    private:
    	int n;
    public:
    	CDemo(int i=0):n(i) { }
    	CDemo & operator++(); //用于前置形式
    	CDemo operator++( int ); //用于后置形式
    	operator int ( ) { return n; }
    	friend CDemo & operator--(CDemo & );
    	friend CDemo operator--(CDemo & ,int);
    };
    CDemo & CDemo::operator++()
    {//前置 ++
    	n ++;
    	return * this;
    }
    CDemo CDemo::operator++(int k )
    { //后置 ++
    	CDemo tmp(*this); //记录修改前的对象
    	n++;
    	return tmp; //返回修改前的对象
    }
    CDemo & operator--(CDemo & d)
    {//前置--
    	d.n--;
    	return d;
    }
    CDemo operator--(CDemo & d,int)
    {//后置--
    	CDemo tmp(d);
    	d.n --;
    	return tmp;
    }
    int main()
    {
    	CDemo d(5);
    	cout << (d++ ) << ","; //等价于 d.operator++(0);
    	cout << d << ",";
    	cout << (++d) << ","; //等价于 d.operator++();
    	cout << d << endl;
    	cout << (d-- ) << ","; //等价于 operator-(d,0);
    	cout << d << ",";
    	cout << (--d) << ","; //等价于 operator-(d);
    	cout << d << endl;
    	return 0;
    }

    程序运行结果:

    5,6,7,7
    7,6,5,5

    本程序将++重载为成员函数,将--重载为全局函数。其实都重载为成员函数更好,这里将--重载为全局函数只是为了说明可以这么做而已。

    调用后置形式的重载函数时,对于那个没用的 int 类型形参,编译器自动以 0 作为实参。 如第 39 行,d++等价于d.operator++(0)

    对比前置++和后置++运算符的重载可以发现,后置++运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp(第21行),而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--运算符的执行效率也比前置的低。

    前置++运算符的返回值类型是 CDemo &,而后置++运算符的返回值类型是 CDemo,这是因为运算符重载最好保持原运算符的用法。C++ 固有的前置++运算符的返回值本来就是操作数的引用,而后置++运算符的返回值则是操作数值修改前的复制品。例如:

    int a = 5;
    (++a) = 2;

    上面两条语句执行后,a 的值是 2,因为 ++a 的返回值是 a 的引用。而

    (a++) = 2;

    这条语句是非法的,因为 a++ 的返回值不是引用,不能作为左值。

    --运算符的返回值类型的设定和++运算符一样。

    在有的编译器(如Visual Studio)中,如果没有后置形式的重载,则后置形式的自增或自减表达式也被当作前置形式处理。而在有的编译器(如Dev C++)中,不进行后置形式的重载,则后置形式的表达式就会编译出错。

    9 C++运算符重载注意事项以及汇总

    在 C++ 中进行运算符重载时,有以下问题需要注意:

    • 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
    • C++ 规定,运算符重载不改变运算符的优先级。
    • 以下运算符不能被重载:..*::? :sizeof
    • 重载运算符()[]->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。

    运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。

    运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。

    运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。

    必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。

    运算符可以重载为全局函数,然后声明为类的友元。

    <<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。

    类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。

    自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。

    运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。

    展开全文
  • Java工程中的主函数public static void main(String[] args)

    千次阅读 多人点赞 2019-09-06 09:03:23
    平时写java project的时候,肯定会看到public static void main(String[] args),一说起来,大家都管它叫“主函数”、“主程序入口”等等。但是它到底用在哪里,其中的String[] args到底都有什么,将在这篇博客中...


    平时写java project的时候,肯定会看到public static void main(String[] args),一说起来,大家都管它叫“主函数”、“Java应用程序的入口”等等。但是它到底用在哪里,其中的String[] args到底都有什么,将在这篇博客中仔细介绍。

    主函数简介

    首先,public static void main(String[] args)是作为Java程序的程序入口,main是JVM识别的特殊方法名,只有包含main()方法的java程序才能够被JVM执行。JVM通过main方法找到需要启动的运行程序,并且检查main函数所在类是否被JVM装载。如果没有装载,那么就装载该类,并且装载该类的所有相关类。因此程序在运行的时候,第一个执行的方法(注意是方法,而不是其他的,例如代码块——为什么这么说,还有代码块在main方法前面调用?待补入本文)就是main()方法。简单点说,JVM运行程序的时候,首先找的就是main方法——你在哪个文件创建了public static void main(String[] args),JVM就从那个文件的public static void main(String[] args)开始执行。jvm在启动时就是按照上诉方法的签名(必须有public和static修饰,返回值为void,且方法参数为字符串数组,待验证)来查找方法的入口地址,若找到就执行,找不到就会报错。

    在新建文件时,会有一项提示“是否新建public static void main(String[] args),选中它,就会在这个新建的文件内生成public static void main(String[] args),待补图

    主函数声明中各关键词的作用

    然后,我们分别来看下public static void main(String[] args)中各个关键字起到的作用:

    public

    main是java程序的入口,java程序通过JVM调用,属于外部调用,所以需要使用public修饰,否则虚拟机无法调用

    static

    在java中,没有static的变量或函数,如果想被调用的话,是要先新建一个对象才可以。而main函数作为程序的入口,需要在其它函数实例化之前就启动。或者更具体地说,通常情况下, 如果要运行一个类的方法,必须首先实例化出这个类的一个对象,然后通过调用该对象的方法——即"对象名.方法名()"的方式来运行,但是因为main是程序的入口,这时候还没有实例化对象,因此将main方法声明为static的,这样main()中的代码是存储在静态存储区的,即当定义了类以后,这段代码就已经存在了。在类加载的时候,静态方法——main()方法就会随之加载到内存中去。如果main()方法没有使用static修饰符,那么编译不会出错,但是当你试图执行该程序时,编译器将会报错,提示main()方法不存在。因为包含main()的类并没有实例化(即没有这个类的对象),所以其main()方法也不会存在。而使用static修饰符则表示该方法是静态的,不需要实例化即可使用。

    void

    从语法上不能写成?)对于java中的main(),jvm有限制,不能有返回值,因此返回值类型为void。因为当main方法出现返回值时,JVM无法进行上抛,如果有返回值难道抛给操作系统么?(JVM外面就是操作系统?)

    区分public static void main(String[] args) throws Exception不能这么写的原因
    从逻辑上不能写成,并非语法上有问题)抛出异常的目的,一般是为了让调用方处理异常,而主函数main()是最终调用者,没有函数再调用它。如果到main函数这里还往外抛出异常,相当于把异常抛给了JVM,而JVM的处理方式就是让程序挂掉,然后打印异常信息,那就相当于写了异常抛出跟没写一样,都是出了异常,程序挂掉,所以说逻辑上来说没有这么写的,但是这么写是不存在任何语法错误

    String[] args

    为开发人员在命令行状态下与程序交互提供了一种手段
    参数String[] args是一个字符串数组,接收来自程序执行时传进来的参数。其中,有博客(文中还提到了命令提示行下,运行Java程序的方法)提到,“args是数组的名字,并不是固定的,可以改成其他名字”——这一点我还没有确认,不确定
    如果是在控制台,可以通过编译执行将参数传入主函数,命令行如下:
    在这里插入图片描述
    这样传进main函数的就是一个字符串数组, args[0] =a; args[1]=b;args[2]=c, 如果不传参数进来,args为空。如下所示:
    在这里插入图片描述
    和上面一样,程序使用者还可以在命令行状态下向某个类传递参数,样例可以看这篇博客,代码的写法可以参考这篇博客

    最后,我在网上还看到了一篇好玩的博客;synchronized写法,其中提出了public static void main(String[] args)函数各种不常见的写法,大家感兴趣的可以去看看。我能力有限,目前判别不出这篇文章内容正确与否,还请各位自己判断这篇博客的对错,见谅。如果有大神能确定这篇文章内容的对或错,也欢迎在本博客下留言告诉我,在这提前说声谢谢。

    一个Java工程中可以有多个主函数

    主函数虽然是Java应用程序的入口,但一个Java工程却可以有多个主函数。工程里的所有代码文件,均以含有主函数的各个class文件为根节点,分成几组,构成多个树形结构。每次在启动Java工程时,eclipse会以当前界面文件为准,打开它属于的根节点文件的主函数

    展开全文
  • 简易入门MFC

    万次阅读 多人点赞 2018-03-26 17:06:31
    目标:做一简单的计算器,代码就不考虑了,主要强调如何上手MFC,和简单了解MFC的框架。1.如何创建一MFC工程项目创建MFC的过程如下:(visual studio 2012)1&gt;.新建-&gt;项目:选择MFC应用程序,名称...
    工作需要用到MFC,需要能快速上手,中间碰到不懂的简单的看了下源码,参考了些资料。


    目标:做一个简单的计算器,代码就不考虑了,主要强调如何上手MFC,和简单了解MFC的框架。


    1.如何创建一个MFC工程项目
    创建MFC的过程如下:(visual studio 2012)
    1>.新建->项目:选择MFC应用程序,名称这里用test(随意,和后面代码那里一致)。然后点确定。

    2>.出现MFC生成向导:这里选择基于对话框,其他默认。

    2.界面设计方式
    2.1.拖拉控件及修改空间属性
    1>.界面设计主要是在这个资源文件中修改。
    2>.控件在工具箱中拖拽出来放到界面上。

    3>.修改控件属性
    单击控件后,可以在属性中修改控件的属性。
    常用的属性
    Caption 标题
    ID 控件标识

    2.2.修改控件布局
    这个没查资料,感觉可以设置布局。
    界面的左边和上方能控制水平和垂直方向上的自动对齐。
    可以在设置好位置后,将控件移动对齐到这个方向的位置,后面拖动这个位置的坐标就可以进行整体对齐移动了。

    3.控件的事件回调函数处理
    双击控件,可自动跳转到点击控件的事件回调处理函数。
    可以在跳转到的函数回调上编写处理代码:
    void CtestDlg::OnBnClickedButton1()
    {
    // TODO: 在此添加控件通知处理程序代码
    CString str = NULL;
    GetDlgItemText(IDC_EDIT1, str);
    SetDlgItemText(IDC_EDIT1, str+_T("1"));
    }
    这里在界面上除自身的代码外比较常用的就是这些属性相关(设置和获取)的函数。
    这部分可以通过搜索引擎或者MSDN解决。

    4.粗略分析自动生成的代码
    4.1.关于自动生成的几个类
    这里可能涉及到框架的部分程序了,这里做简单的了解。
    首先最简单的Dialog中间有这几个类:CAboutDlg,CtestApp,CtestDlg。
    其中CAboutDlg类和CtestDlg类被放到了testDlg.cpp中实现。
    1>.CAboutDlg
    CAboutDlg是用于应用程序“关于”菜单项的 CAboutDlg 对话框。估计是这个关于的对话框。在.rc的资源中的dialog中的IDD_ABOUTBOX中可修改。。
    这部分是CAboutDlg的代码,可以看出一个简单的对话框窗口:
    1.继承CDialogEx类;
    2.有自己的映射关系;
    3.DoDataExchange函数;这个函数在MFC框架中的UpdateData会调用。
    4.类中声明的IDD枚举???

    class CAboutDlg : public CDialogEx
    {
    public:
    CAboutDlg();
    enum { IDD = IDD_ABOUTBOX };// 对话框数据

    protected:
    virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持

    // 实现
    protected:
    DECLARE_MESSAGE_MAP()
    };

    CAboutDlg::CAboutDlg() : CDialogEx(CAboutDlg::IDD){}

    void CAboutDlg::DoDataExchange(CDataExchange* pDX)
    {
    CDialogEx::DoDataExchange(pDX);
    }

    BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
    END_MESSAGE_MAP()

    2>.CtestDlg
    所以CtestDlg应该是主界面的代码。结构和上述类似。只是增加了几个消息回调函数。但中间有结果回调函数比较特殊。应该是和后面几个消息回调连着的,可能不需要给用户自定义,连名字都省略了= =
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    注:afx_msg 是个标识,来表示MFC的消息处理

    OnQueryDragIcon从注释上意思:当用户拖动最小化窗口时系统调用此函数取得光标
    OnPaint从注释上意思:来绘制该图标。对于使用文档/视图模型的 MFC 应用程序。在初始化后会向窗口发送WM_PAINT消息,然后框架自动回调。这部分的代码也是由框架封装好的CDialogEx::OnPaint(),可以跳转进去看看,用来绘制图形的。
    OnSysCommand不详。。。
    另外:BOOL CtestDlg::OnInitDialog()是初始化消息的回调函数。用来初始化窗口。

    3>.CtestApp
    CtestApp类在test.h/test.c中,继承了CWinApp,定义应用程序的类行为,且有唯一一个对象theApp,在一开始就会被构造。
    这个类中的构造函数添加了支持重新启动管理器的flag。
    另一个方法是初始化实例InitInstance。

    这里需要提到MFC框架的入口和初始化过程:
    theApp在一开始就被构造,初始化了一些变量,然后将当前应用指到本身。
    MFC的入口在源代码的APPMODUL.CPP文件中的_tWinMain函数中。_tWinMain中调用了AfxWinMain函数。
    AfxWinMain为一个MFC框架的 全局函数 。在这个函数中模仿了WIN32的创建一个应用的过程:
    1.获得一个CWinThread和app的指针(CWinApp也是继承CWinThread的,通过宏可知这两个指针一样。)
    2.通过APP指针初始化应用(InitApplication),初始化CWinThread指针的实例(InitInstance方法)。
    InitApplication做框架的内部管理工作。
    这里InitInstance方法就调用CtestApp重写的部分(这部分代码其实也是系统自动生成的)。这个函数完成了窗口CtestDlg创建,显示,并向窗口发送WM_PAINT消息的功能。
    注:这里好像不同的写法(使用方式)中间的调用过程也不大一样。如果是前面那种方式在DoModal里就调用了DoDataExchange函数来加载资源??
    3.运行theApp

    4.2.关于函数回调部分的代码
    前面说双击回调后,会自动跳转生成的代码。这里自动生成的部分主要有:
    回调处理函数:
    void CtestDlg::OnBnClickedButton1()
    {
    // TODO: 在此添加控件通知处理程序代码
    }

    关系映射:
    BEGIN_MESSAGE_MAP(CtestDlg, CDialogEx)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_BUTTON1, &CtestDlg::OnBnClickedButton1)
    ON_BN_CLICKED(IDC_BUTTON2, &CtestDlg::OnBnClickedButton2)
    ON_BN_CLICKED(IDC_BUTTON3, &CtestDlg::OnBnClickedButton3)
    ON_BN_CLICKED(IDC_BUTTON4, &CtestDlg::OnBnClickedButton4)
    ON_BN_CLICKED(IDC_BUTTON5, &CtestDlg::OnBnClickedButton5)
    ON_BN_CLICKED(IDC_BUTTON6, &CtestDlg::OnBnClickedButton6)
    ON_BN_CLICKED(IDC_BUTTON7, &CtestDlg::OnBnClickedButton7)
    ON_BN_CLICKED(IDC_BUTTON8, &CtestDlg::OnBnClickedButton8)
    ON_BN_CLICKED(IDC_BUTTON9, &CtestDlg::OnBnClickedButton9)
    ON_BN_CLICKED(IDC_BUTTON10, &CtestDlg::OnBnClickedButton10)
    END_MESSAGE_MAP()

    声明:
    afx_msg void OnEnChangeEdit1();

    调到宏里面看看原理:(截取了一段button的)
    // User Button Notification Codes
    #define ON_BN_CLICKED(id, memberFxn) \
    ON_CONTROL(BN_CLICKED, id, memberFxn)
    #define ON_BN_DOUBLECLICKED(id, memberFxn) \
    ON_CONTROL(BN_DOUBLECLICKED, id, memberFxn)
    #define ON_BN_SETFOCUS(id, memberFxn) \
    ON_CONTROL(BN_SETFOCUS, id, memberFxn)
    #define ON_BN_KILLFOCUS(id, memberFxn) \
    ON_CONTROL(BN_KILLFOCUS, id, memberFxn)

    定义了几种动作的宏(每种控件间会有差异),将控件标识和对应的动作类型和对应的响应回调函数连接起来。宏里面的结构如下,看上去像一个表的形式。
    // for general controls
    #define ON_CONTROL(wNotifyCode, id, memberFxn) \
    { WM_COMMAND, (WORD)wNotifyCode, (WORD)id, (WORD)id, AfxSigCmd_v, \
    (static_cast< AFX_PMSG > (memberFxn)) },

    5.其他注意事项
    5.1.关于命名的问题
    命名的规则:


    5.2.MFC自定义的类型
    第一次用MFC中,出现了一些类型,比如CString,可以在afx.h头文件中查看它的定义。另外使用MSDN可以查看。
    另外:这里要注意字符集的问题。可以用_T("XXXX")来统一字符集。

    5.3.窗口中定义的控件
    拖进界面生成的控件的资源被,按照一定的组织格式放在了test.rc这个文件中。MFC框架中在CtestApp::InitInstance()里就加载这个.rc的文件的部分并进行了显示。
    注*:这里的控件也可在窗口创建的回调函数中创建。只不过这里的方法不同。

    5.4.关于一些辅助的宏
    看到了随便加进来一下:
    ①. TRACE
    TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
    追踪路径,先看TRACE的定义:
    #define TRACE ATLTRACE
    #define ATLTRACE ATL::CTraceFileAndLineInfo(__FILE__, __LINE__)
    void __cdecl operator()(
    _In_ DWORD_PTR dwCategory,
    _In_ UINT nLevel,
    _In_z_ const char *pszFmt,
    ...) const
    {
    va_list ptr; va_start(ptr, pszFmt);
    ATL::CTrace::s_trace.TraceV(m_pszFileName, m_nLineNo, dwCategory, nLevel, pszFmt, ptr);
    va_end(ptr);
    }
    再往下好像封装进去了,大概可以通过这个宏,可以把追踪信息文件,行数,必要的提示,警告等级之类的都显示下来。
    TRACE宏只对Debug 版本的工程产生作用。

    ②.ASSERT
    编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是 异常处理 的一种高级形式。断言表示为一些 布尔表达式 ,程序员相信在程序中的某个特定点该表达式值为真。可以在任何时候启用和禁用断言验证, 因此可以在测试时启用断言,而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。

    看看MFC中的ASSERT。
    #define ASSERT(f) DEBUG_ONLY((void) ((f) || !::AfxAssertFailedLine(THIS_FILE, __LINE__) || (AfxDebugBreak(), 0)))
    #define DEBUG_ONLY(f) (f)
    相当于DEBUG模式下,直接判断ASSERT中的表达式f是否满足条件。如果满足表达式满足,就会调用AfxAssertFailedLine,然后中断退出。

    6.相关参考
    VC++深入讲解

    7.总结
    在大致看完之后,MFC其实就是Windows下的API进行进一步的封装。如果直接在.rc文件中设计,只需添加对应的回调函数再加处理即可。这样比较快。
    另外一种是对MFC提供出来的框架的部分进行重写(根据自己需求),但大体上也是封装Windows的API的过程,这样会相对麻烦一些。

    展开全文
  • 写一个函数,接收一个整数,输出这个整数是位数 import java.util.Scanner; public class TestInt { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print(...
  • Input常用的几个函数

    千次阅读 2016-03-05 15:52:17
    键盘相关的输入函数:  1. public static bool GetKey(string name);  描述:  当用户保持某个键(name)为按下状态时返回true。比如自动发射。  要处理输入的时候推荐使用Input.GetAxis 和 Input.GetButton, ...
  • C++默认构造函数

    万次阅读 多人点赞 2018-02-08 12:43:51
    本文围绕3问题来理解C++的默认构造函数: 什么是默认构造函数? 默认构造函数什么时候被调用? 编译器在什么情况下会生成默认构造函数?...根据这原则,下面2种构造函数都是默认构造函数: class S...
  • 自定义View的四构造函数

    千次阅读 2019-03-29 13:51:16
    自定义View继承View或者ViewGroup都会让我们实现构造函数,通常会实现一参数的构造函数,两参数的构造函数和三参数的构造函数,它们有什么区别,又为什么要实现这么多构造函数呢? public class DemoView77 ...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
    5目标文件,演示Address EJB的实现,创建一EJB测试客户端,得到名字上下文,查询jndi名,通过强制转型得到Home接口,getInitialContext()函数返回一经过初始化的上下文,用client的getHome()函数调用Home接口...
  • C语言函数

    万次阅读 多人点赞 2019-08-03 19:54:19
    每个C程序都至少有一个函数,即主函数main(),如果程序的任务比较简单,全部的代码都写在main()函数中,但是,在实际开发中,程序的任务往往比较复杂,如果全部的代码都写在main()函数中,main()函数将非常庞大臃肿...
  • 1. 深拷贝和浅拷贝(拷贝构造函数的使用) 有时候需要自己定义拷贝构造函数,以避免浅拷贝问题。...(1)用类的一对象去初始化另一对象时 (2)当函数的形参是类的对象时(也就是值传递时),如果...
  • C++类模板 template 详细使用方法

    万次阅读 多人点赞 2017-11-27 21:48:30
    C++类模板 template 详细使用...有时,有两或多类,其功能是相同的,仅仅是数据类型不同,如下面语句声明了一类: class Compare_int { public : Compare(int a,int b) { x=a; y=b; } int max( ) { re
  • 在Solidity中一个函数方法是可以返回多个结果的。下面示例中完整的展示了,如何定义多返回值的函数,并且如何获取参数。 pragma solidity ^0.5.10; contract ManyReturns{ // 基础方法:返回多个参数,用于被调用...
  • 【C++】C++类和对象、构造函数和析构函数

    千次阅读 多人点赞 2018-06-03 20:32:37
    它即可包含描述事物的数据,又可包含处理这些数据的函数,类在程序运行时是被用作样板来建立对象的。所以要建立对象,首先必须定义类。 定义类 定义一类的一般格式为: class 类名{ private: 成员表1; public:...
  • 浅谈C++中的种构造函数

    万次阅读 多人点赞 2016-04-14 17:56:05
    3月中旬参加宣讲会,做了X软公司的C++笔试题,里面有一道“默认拷贝构造函数的题”,由于好久没复习C++基础知识,当时连基本的概念都想不来了了。于是乎,开始拿起以前看的谭浩强C++复习起来,现在书快要要啃完了,...
  • 运算符重载的原理是:一运算符只是一具有特定意义的符号,只要我们告诉编译程序在什么情况下如何去完成特定的操作,而这种操作的本质是通过特定的函数完成的。 重载运算符 为了重载运算符,首先要定义运算符...
  • C++ public、protected 、 private和friend(最通俗易懂)

    千次阅读 多人点赞 2018-08-29 20:35:44
    当创建一类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一已有的类的成员即可。这已有的类称为基类,新建的类称为派生类。 继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳...
  • Kotlin系列之扩展函数

    万次阅读 2018-04-17 00:22:18
    简述: 今天带来的是Kotlin浅谈系列的第五弹,这讲主要是讲利用Kotlin中的...我会从以下几个方面阐述Kotlin中的扩展函数。 1、为什么要使用Kotlin中的扩展函数? 2、怎么去使用扩展函数和扩展属性? 3、什么是扩展...
  • C++类成员函数指针使用介绍

    万次阅读 多人点赞 2019-09-21 16:07:49
    前言 在之前写过的博客中有介绍过函数指针和指针函数的区别和简单用法(文章在这里),当时的Demo...所以如果是C++中的成员函数指针其使用方法是有区别的,这里针对不同的场景做补充说明。 类成员函数的指针(非...
  • 个函数接受两个整型变量,返回他们的和,但如果我还要一些功能,比如两个double类型的和,一个整型和100的和,并且我也想用add这个函数名怎么办? 函数重载的概念就是用来解决这个问题的,我们...
  • 用C语言写自己的printf函数

    万次阅读 多人点赞 2016-11-04 17:23:28
    上面这是我在Dev-C++上复制过来的函数声明,其实无论是哪个版本声明部分作用都是一样的。 接一下我们看一下printf的返回值是什么玩意?   上图就是我自己实际测试得出来的一结果。 6621! ...
  • C++的函数重载

    万次阅读 多人点赞 2018-07-11 13:44:56
    ——每现象后面都隐藏一本质,关键在于我们是否去挖掘 写在前面: 函数重载的重要性不言而明,但是你知道C++中函数重载是如何实现的呢(虽然本文谈的是C++中函数重载的实现,但我想其它语言也是类似的)
  • C++虚函数的实现

    千次阅读 2019-05-09 16:41:17
    http://blog.kongfy.com/2015/08/探索c虚函数在g中的实现/?utm_source=tuicool&utm_medium=referral ...一、虚函数表解析 前言 虚函数表 一般继承(无虚函数覆盖) 一般继承(有虚函数覆盖) 多重继承(无...
  • c#延时函数,不止Sleep函数

    千次阅读 2019-05-01 13:47:04
    在C#窗口程序中,如果在主线程里...下边实现的是非独占性延时函数,延时过时中界面仍可响应消息: public static void Delay(int milliSecond) { int start = Environment.TickCount; while (Math.Abs(Environme...
  • c#的构造函数及构造函数的调用

    千次阅读 2015-12-01 16:53:07
    C#构造函数的特性一、 什么是C#构造函数?Construct,Function  C#构造函数是一种特殊的成员函数,它主要用于为对象分配存储空间,对数据成员进行初始化.  C#构造函数具有一些特殊的性质:  (1)C#构造...
  • Visual Basic快捷教程——函数与子程序

    千次阅读 多人点赞 2017-04-19 22:30:14
    函数的本质就是为了把程序切分成若干相对独立的模块各司其职,而各个函数的作用也就是仅仅专门负责自己份内的那份功能。在Visual Basic中,函数是一组以Function开头和End Function结尾的封闭程序代码语句。当函数...
  • C++中同名函数之间的关系

    万次阅读 2018-07-24 23:24:45
    参数列表不同(参数类型不同,或者参数数不同,或者参数数和参数类型都不相同);返回类型随意。 覆盖(override):不同作用域下(分别在父类和子类中);函数名相同;参数列表列表相同;返回类型相同(协变...
  • 详解c++中类的六默认的成员函数

    万次阅读 多人点赞 2016-07-06 13:01:35
    类的6默认的成员函数包括: 构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载、const 修饰的取地址操作符重载。 (一)构造函数 构造函数,顾名思义,为对象分配空间,进行初始化。它是一...
  • C++函数模板(模板函数)详解

    万次阅读 多人点赞 2019-07-04 16:03:01
    C++函数模板(模板函数)详解定义用法:函数模板的原理延申用法...所有的类模板函数写在类的外部,在一cpp中2.5总结关于类模板的点说明:2.6类模板中的static关键字案例2:以下来自:C++类模板遇上static关键字...
  • 构造函数和析构函数

    万次阅读 2018-05-30 11:24:57
    通过C++ Primer重新回顾构造函数和析构函数,发现真的好多都忘了… 构造函数 ...对于构造函数有以下点需要注意: 构造函数与类同名,并且没有返回值。 构造函数和其他成员函数一样,都能够...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 614,763
精华内容 245,905
关键字:

下面哪几个函数是public