Salesforce uses some confusing terminology around this topic. For clarity...

 - An aggregate query (as far as governor limits and error messages seem to go) is a parent-child subquery (a.k.a. a "left outer join" in more standard SQL)
 - An aggregate function is something like `COUNT()`, `MAX()`, `AVG()`, etc...

Aggregate functions can only be applied on the outermost query, and most (if not all) require you to also use `GROUP BY`. This causes the query to return a `List<AggregateResult>` rather than a `List<SObject>`.

Both parent-child subqueries and aggregate functions have their uses. In the particular example that you've given, the advantage of using `COUNT()` is that you don't need to worry about the number of records returned. It'll get you the number you're looking for with minimal fuss.

With a parent-child subquery, however, if you have enough child records you won't be able to simply call `parent.children.size()`. At a certain point, Salesforce will try to call `queryMore` (internally) and you'll be greeted with an error message telling you to iterate over the child records.

That means that your simple `parent.children.size()` becomes (pseudocode)
```java
for(parent :[parent query]){
    List<child> childList;

    for(child :parent.children){
        childList.add(child);
    }

    childList.size();
}
```

So in this case, the aggregate functions end up being less work (SOQL does more of the work for you). Things like doing your own rollups are a good use case for aggregate functions as well. In other cases, the parent-child subquery approach may make the rest of your code simpler.

At the end of the day, it's about doing whatever makes your life easier. If there's a reasonable way to foist some of the work onto SOQL (or another tool/feature) that would normally be done via apex, it's worth looking into.