AleksanderNaumenok
AleksanderNaumenok

Reputation: 161

C++ working with objects on heap

I am currently learning C++, coming from C#/Java background, using visual studio 2017.

I have a question in regards to creating objects on heap and referencing them properly down the road. So far I came across multiple tutorials and ways of doing things. Some recommend using smart pointers as much as possible, others swear its the devils tool.

My current main looks like this:

//Main
Person *makePerson()
{
    string name;
    int age;
    cout << "Input name: ";
    cin >> name;
    cout << "Input age: ";
    cin >> age;
    return new Person(name, age);
}

Child *makeChild(Person &parent)
{
    return new Child(*makePerson(), &parent);;
}

int main()
{
    cout << "---Input parent data---" << endl;
    Person *person = makePerson();
    cout << "printing: " << *person << endl;
    cout << "---Input child data---" << endl;
    Child *child = makeChild(*person);
    cout << "printing: " << *child << endl;
    cout << "---end of main---" << endl;
    delete person;
    delete child;
    return 0;
}

A function handles input of personal data and returns a pointer to new Person object. Then I have a function that handles creation of child object by taking a parent reference and asking makePerson for remaining data.

Can this be considered good C++? How can I make it better? I would really appreciate some code examples.

As some have already suggested, I could replace raw pointers with either shared_ptr<Person> person (heavy) or unique_ptr<Person> (better than shared).

This is code for Person and child classes. Note that Child has a raw pointer of type Person *parent.

//header
class Person
{
protected: 
    std::string name;
    int age;
public:
    Person();
    Person(const Person& other);
    Person(std::string inName, int inAge);
    ~Person();
    virtual void print() const;
    std::string getName() const;
    int getAge() const;
    Person &operator=(const Person &other);
    //overload print functionality, act as if it was toString
    friend std::ostream &operator<<(std::ostream &out, const Person &p);
};
//cpp
Person::Person() : name(""), age(0) {
    std::cout << "Person empty constructor" << std::endl;
}

Person::Person(std::string inName, int inAge) : name(inName), age(inAge) {
    std::cout << "Person (" << name << ") default constructor" << std::endl;
}

Person::Person(const Person & other) : name(other.name), age(other.age) {
    std::cout << "Person (" << name << ") copy constructor" << std::endl;
}

Person::~Person() {
    std::cout << "Person (" << name << ") destructor" << std::endl;
}

void Person::print() const {
    std::cout << name << ", " << age << std::endl;
}

std::string Person::getName() const
{
    return name;
}

int Person::getAge() const
{
    return age;
}

Person & Person::operator=(const Person & other) {
    std::cout << "Person (" << other.name << ") assignment constructor" << std::endl;
    name = other.name;
    age = other.age;
    return *this;
}
std::ostream &operator<<(std::ostream &out, const Person &p) {
    return out << p.name << ", " << p.age;
}

A child is a person and it makes sense for a child to have knowledge of who childs parent is. However, am uncertain how to handle this "knowledge". Here is code I am using for child class:

//Header
class Child : public Person
{
private:
    const Person *parent;
public:
    Child();
    Child(std::string name, int age);
    Child(std::string name, int age, const Person *parent);
    Child(const Child &child, const Person *parent);
    Child(const Person &person);
    ~Child();
    Child &operator=(const Child &other);
    void print() const;
    friend std::ostream &operator<<(std::ostream &out, const Child &c);
};
//cpp
Child::Child() {
    std::cout << "Child empty constructor" << std::endl;
}

Child::Child(std::string name, int age) : Person(name, age), parent(nullptr) {
    std::cout << "Orphan (" << name << ") constructor" << std::endl;
}

Child::Child(std::string name, int age, const Person *parent) :
    Person(name, age), parent(parent) {
    std::cout << "Child (" << name << ") default constructor" << std::endl;
}

Child::Child(const Child &child, const Person *parent) : 
    Person(child.name, child.age), parent(parent) {
    std::cout << "Child (" << child.name << ") copy constructor" << std::endl;
}

Child::Child(const Person &person) : Person(person), parent(nullptr) {
    std::cout << "Child from person (" << name << ") constructor" << std::endl;
}

Child::~Child() {
    std::cout << "Child (" << name << ") destructor" << std::endl;
}

Child & Child::operator=(const Child & other) {
    name = other.name;
    age = other.age;
    parent = other.parent;
    std::cout << "Child (" << name << ") assignment constructor" << std::endl;
    return *this;
}

void Child::print() const {
    if(parent)
        std::cout << *this << " is child of " << *parent << std::endl;
    else
        std::cout << *this << " is orphan" << std::endl;
}

std::ostream &operator<<(std::ostream &out, const Child &c) {
    return out << c.name << ", " << c.age << " is " << 
        (c.parent ? ("child of " + c.parent->getName() + ", " + std::to_string(c.parent->getAge())) : "orphan");
}

This is the output I get:

enter image description here

I suppose my question still remains, could anyone give me an example of how it should look like to be considered good C++?

@user4581301 if you take a look at the updated main, do you mean I should return a std::unique_ptrinstead of a * (raw pointer)? In which case my function will look like this:

std::unique_ptr<Person> makePerson2()
{
    string name;
    int age;
    cout << "Input name: ";
    cin >> name;
    cout << "Input age: ";
    cin >> age;
    return std::unique_ptr<Person>(new Person(name, age));
}

And variable declaration as:

std::unique_ptr<Person> upParent = makePerson2();
cout << "printing: " << *upParent << endl;

Would this be considered "better" C++ than what I have so far?

Upvotes: 4

Views: 1339

Answers (1)

David Haim
David Haim

Reputation: 26476

I think that one of the most important thing to know about C++ especially if you come from Java/C# background is:

Objects are value-types by default, not reference type!

you entire code could have been written as simply:

int main()
{
    //this work
    Person person("John Doe", 22); 
    //this work
    Child child("Johnny Doe", 2, person);
    cout << "---end of main---" << endl;
    return 0;
}

See how the code turned into nothing? you don't have to worry about allocations, deleting unused object etc. because objects are not reference type to begin with!

My personal hierarchy of rules go:

  1. use objects as value types as much as possible. pass them by reference to avoid copies. make sure a valid move constructor is implemented for relevant classes. objects as value types + references should be the default way of programming C++.
  2. if you can't use a reference, because you want to specify a missing object, use C-pointer. by any way, strict the C-pointers to minimum and never let them manage anything. C-pointers are basically "viewers" to something and they can view "nothing" (or null). always think if you can replace C-pointer by C++ reference. if you can do it, do it.
  3. use std::unique_ptr if for some reason you do need dynamic memory allocation, such as dynamic polymorphism. keep in mind that C++ works mostly on templates as static polymorphism, instead of the Java style inherit+override technique.
  4. use std::shared_ptr rarely,and only when you're sure there are many owners, like an object which is referenced on different threads. std::shared_ptr should be used in extreme cases. always copy the shared pointer. never pass the shared pointer by reference.

Anyway, new , new[] , delete and delete[] are pretty much deprecated. only library writers should use them in very extreme cases. std::make_ should be the only way allocating object on the heap, alongside the STL containers such as std::vector and std::list.

Upvotes: 5

Related Questions